From 5068828ca703411491661d4218d5055ab8cdc890 Mon Sep 17 00:00:00 2001 From: vicanso Date: Sun, 15 May 2022 15:07:03 +0800 Subject: [PATCH 001/116] feat: support painter for chart draw function --- alias.go | 33 +++++ line.go | 9 +- painer.go | 394 +++++++++++++++++++++++++++++++++++++++++++++++++ painer_test.go | 371 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 802 insertions(+), 5 deletions(-) create mode 100644 alias.go create mode 100644 painer.go create mode 100644 painer_test.go 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: "\\n", + }, + // circle + { + fn: func(p *Painter) { + p.Circle(5, 2, 3) + }, + result: "\\n", + }, + // text + { + fn: func(p *Painter) { + p.Text("hello world!", 3, 6) + }, + result: "\\nhello world!", + }, + // line stroke + { + fn: func(p *Painter) { + p.LineStroke([]Point{ + { + X: 1, + Y: 2, + }, + { + X: 3, + Y: 4, + }, + }, LineStyle{ + StrokeColor: drawing.ColorBlack, + StrokeWidth: 1, + }) + }, + result: "\\n", + }, + // set background + { + fn: func(p *Painter) { + p.SetBackground(400, 300, chart.ColorWhite) + }, + result: "\\n", + }, + // 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: "\\n", + }, + // 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: "\\n", + }, + // 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: "\\n", + }, + // 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: "\\n", + }, + // 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: "\\n", + }, + // 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: "\\n", + }, + // 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: "\\n", + }, + // 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: "\\n", + }, + // 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: "\\n", + }, + } + 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(`\nHelloWorld!Hello World!`, string(buf)) +} From 7e80e9a8481d73b4a10d6257a1c07450f886cd9e Mon Sep 17 00:00:00 2001 From: vicanso Date: Mon, 16 May 2022 20:41:13 +0800 Subject: [PATCH 002/116] refactor: adjust axis function --- axis.go | 118 +++++++++++++++--------------- axis_test.go | 14 ++-- painer.go => painter.go | 56 ++++++++++++-- painer_test.go => painter_test.go | 0 util.go | 4 +- xaxis.go | 21 ++---- yaxis.go | 28 +++---- 7 files changed, 134 insertions(+), 107 deletions(-) rename painer.go => painter.go (88%) rename painer_test.go => painter_test.go (100%) diff --git a/axis.go b/axis.go index 46292e4..5881f5e 100644 --- a/axis.go +++ b/axis.go @@ -27,7 +27,6 @@ import ( "github.com/golang/freetype/truetype" "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" ) type AxisOption struct { @@ -42,7 +41,7 @@ type AxisOption struct { SplitNumber int ClassName string // The line color of axis - StrokeColor drawing.Color + StrokeColor Color // The line width StrokeWidth float64 // The length of the axis tick @@ -56,17 +55,17 @@ type AxisOption struct { // The font of label Font *truetype.Font // The color of label - FontColor drawing.Color + 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 drawing.Color + SplitLineColor Color } type axis struct { - d *Draw - data *AxisDataList - option *AxisOption + painter *Painter + data *AxisDataList + option *AxisOption } type axisMeasurement struct { Width int @@ -74,11 +73,11 @@ type axisMeasurement struct { } // NewAxis creates a new axis with data and style options -func NewAxis(d *Draw, data AxisDataList, option AxisOption) *axis { +func NewAxis(p *Painter, data AxisDataList, option AxisOption) *axis { return &axis{ - d: d, - data: &data, - option: &option, + painter: p, + data: &data, + option: &option, } } @@ -149,17 +148,19 @@ func NewAxisDataListFromStringList(textList []string) AxisDataList { func (a *axis) axisLabel(renderOpt *axisRenderOption) { option := a.option data := *a.data - d := a.d + // d := a.d if option.FontColor.IsZero() || len(data) == 0 { return } - r := d.Render + // r := d.Render - s := option.Style(d.Font) - s.GetTextOptions().WriteTextOptionsToRenderer(r) + // s.GetTextOptions().WriteTextOptionsToRenderer(r) + p := a.painter + s := option.Style(p.font) + p.SetTextStyle(s) - width := d.Box.Width() - height := d.Box.Height() + width := p.Width() + height := p.Height() textList := data.TextList() count := len(textList) @@ -188,7 +189,7 @@ func (a *axis) axisLabel(renderOpt *axisRenderOption) { reverseIntSlice(values) for index, text := range textList { y := values[index] - 2 - b := r.MeasureText(text) + b := p.MeasureText(text) if boundaryGap { height := y - values[index+1] y -= (height - b.Height()) >> 1 @@ -200,7 +201,7 @@ func (a *axis) axisLabel(renderOpt *axisRenderOption) { if position == PositionLeft { x = labelWidth - b.Width() - 1 } - d.text(text, x, y) + p.Text(text, x, y) } default: // 定位bottom,重新计算y0的定位 @@ -215,7 +216,7 @@ func (a *axis) axisLabel(renderOpt *axisRenderOption) { } x := values[index] leftOffset := 0 - b := r.MeasureText(text) + b := p.MeasureText(text) if boundaryGap { width := values[index+1] - x leftOffset = (width - b.Width()) >> 1 @@ -223,24 +224,25 @@ func (a *axis) axisLabel(renderOpt *axisRenderOption) { // 左移文本长度 leftOffset = -b.Width() >> 1 } - d.text(text, x+leftOffset, y0) + p.Text(text, x+leftOffset, y0) } } } func (a *axis) axisLine(renderOpt *axisRenderOption) { - d := a.d - r := d.Render + // d := a.d + // r := d.Render + p := a.painter option := a.option - s := option.Style(d.Font) - s.GetStrokeOptions().WriteDrawingOptionsToRenderer(r) + s := option.Style(p.font) + p.SetDrawingStyle(s.GetStrokeOptions()) x0 := 0 y0 := 0 x1 := 0 y1 := 0 - width := d.Box.Width() - height := d.Box.Height() + width := p.Width() + height := p.Height() labelMargin := option.GetLabelMargin() // 轴线 @@ -271,21 +273,22 @@ func (a *axis) axisLine(renderOpt *axisRenderOption) { y1 = y0 } - d.moveTo(x0, y0) - d.lineTo(x1, y1) - r.FillStroke() + p.MoveTo(x0, y0) + p.LineTo(x1, y1) + p.FillStroke() } func (a *axis) axisTick(renderOpt *axisRenderOption) { - d := a.d - r := d.Render + // d := a.d + // r := d.Render + p := a.painter option := a.option - s := option.Style(d.Font) - s.GetStrokeOptions().WriteDrawingOptionsToRenderer(r) + s := option.Style(p.font) + p.SetDrawingStyle(s.GetStrokeOptions()) - width := d.Box.Width() - height := d.Box.Height() + width := p.Width() + height := p.Height() data := *a.data tickCount := len(data) if tickCount == 0 { @@ -319,14 +322,14 @@ func (a *axis) axisTick(renderOpt *axisRenderOption) { for _, v := range values { x := x0 y := v - d.moveTo(x, y) - d.lineTo(x+tickLengthValue, y) - r.Stroke() + p.MoveTo(x, y) + p.LineTo(x+tickLengthValue, y) + p.Stroke() } } // 辅助线 if option.SplitLineShow && !option.SplitLineColor.IsZero() { - r.SetStrokeColor(option.SplitLineColor) + p.SetStrokeColor(option.SplitLineColor) splitLineWidth := width - labelWidth - tickLengthValue x0 = labelWidth + tickLengthValue if position == PositionRight { @@ -336,9 +339,9 @@ func (a *axis) axisTick(renderOpt *axisRenderOption) { for _, v := range values[0 : len(values)-1] { x := x0 y := v - d.moveTo(x, y) - d.lineTo(x+splitLineWidth, y) - r.Stroke() + p.MoveTo(x, y) + p.LineTo(x+splitLineWidth, y) + p.Stroke() } } default: @@ -355,14 +358,14 @@ func (a *axis) axisTick(renderOpt *axisRenderOption) { } x := v y := y0 - d.moveTo(x, y-tickLengthValue) - d.lineTo(x, y) - r.Stroke() + p.MoveTo(x, y-tickLengthValue) + p.LineTo(x, y) + p.Stroke() } } // 辅助线 if option.SplitLineShow && !option.SplitLineColor.IsZero() { - r.SetStrokeColor(option.SplitLineColor) + p.SetStrokeColor(option.SplitLineColor) y0 = 0 splitLineHeight := height - labelHeight - tickLengthValue if position == PositionTop { @@ -377,22 +380,23 @@ func (a *axis) axisTick(renderOpt *axisRenderOption) { x := v y := y0 - d.moveTo(x, y) - d.lineTo(x, y0+splitLineHeight) - r.Stroke() + p.MoveTo(x, y) + p.LineTo(x, y0+splitLineHeight) + p.Stroke() } } } } func (a *axis) measureTextMaxWidthHeight() (int, int) { - d := a.d - r := d.Render - s := a.option.Style(d.Font) + // d := a.d + // r := d.Render + p := a.painter + s := a.option.Style(p.font) data := a.data - s.GetStrokeOptions().WriteDrawingOptionsToRenderer(r) - s.GetTextOptions().WriteTextOptionsToRenderer(r) - return measureTextMaxWidthHeight(data.TextList(), r) + p.SetDrawingStyle(s.GetStrokeOptions()) + p.SetTextStyle(s.GetTextOptions()) + return measureTextMaxWidthHeight(data.TextList(), p) } // measure returns the measurement of axis. @@ -429,7 +433,7 @@ func (a *axis) Render() { } unitCount := chart.MaxInt(option.SplitNumber, 1) - width := a.d.Box.Width() + width := a.painter.Width() textList := a.data.TextList() count := len(textList) diff --git a/axis_test.go b/axis_test.go index 37c8314..fe253e9 100644 --- a/axis_test.go +++ b/axis_test.go @@ -207,10 +207,10 @@ func TestAxis(t *testing.T) { }, } for _, tt := range tests { - d, err := NewDraw(DrawOption{ + p, err := NewPainter(PainterOptions{ Width: 400, Height: 300, - }, PaddingOption(chart.Box{ + }, PainterPaddingOption(chart.Box{ Left: 5, Top: 5, Right: 5, @@ -222,9 +222,9 @@ func TestAxis(t *testing.T) { if tt.newData != nil { data = tt.newData() } - NewAxis(d, data, style).Render() + NewAxis(p, data, style).Render() - result, err := d.Bytes() + result, err := p.Bytes() assert.Nil(err) assert.Equal(tt.result, string(result)) } @@ -233,7 +233,7 @@ func TestAxis(t *testing.T) { func TestMeasureAxis(t *testing.T) { assert := assert.New(t) - d, err := NewDraw(DrawOption{ + p, err := NewPainter(PainterOptions{ Width: 400, Height: 300, }) @@ -243,14 +243,14 @@ func TestMeasureAxis(t *testing.T) { "Sun", }) f, _ := chart.GetDefaultFont() - width := NewAxis(d, data, AxisOption{ + width := NewAxis(p, data, AxisOption{ FontSize: 12, Font: f, Position: PositionLeft, }).measure().Width assert.Equal(44, width) - height := NewAxis(d, data, AxisOption{ + height := NewAxis(p, data, AxisOption{ FontSize: 12, Font: f, Position: PositionTop, diff --git a/painer.go b/painter.go similarity index 88% rename from painer.go rename to painter.go index 670f6f6..639371e 100644 --- a/painer.go +++ b/painter.go @@ -38,14 +38,15 @@ type Painter struct { parent *Painter style Style previousStyle Style + theme *Theme } type PainterOptions struct { // Draw type, "svg" or "png", default type is "svg" Type string - // The width of draw canvas + // The width of draw painter Width int - // The height of draw canvas + // The height of draw painter Height int // The font for painter Font *truetype.Font @@ -53,7 +54,7 @@ type PainterOptions struct { type PainterOption func(*Painter) -// PainterPaddingOption sets the padding of draw canvas +// PainterPaddingOption sets the padding of draw painter func PainterPaddingOption(padding Box) PainterOption { return func(p *Painter) { p.box.Left += padding.Left @@ -63,7 +64,7 @@ func PainterPaddingOption(padding Box) PainterOption { } } -// PainterBoxOption sets the box of draw canvas +// PainterBoxOption sets the box of draw painter func PainterBoxOption(box Box) PainterOption { return func(p *Painter) { if box.IsZero() { @@ -73,21 +74,45 @@ func PainterBoxOption(box Box) PainterOption { } } -// PainterFontOption sets the font of draw canvas +// 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 canvas +// PainterStyleOption sets the style of draw painter func PainterStyleOption(style Style) PainterOption { return func(p *Painter) { p.SetDrawingStyle(style) } } -// NewPainter creates a new Painter +// PainterThemeOption sets the theme of draw painter +func PainterThemeOption(theme *Theme) 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") @@ -135,6 +160,7 @@ func (p *Painter) Child(opt ...PainterOption) *Painter { parent: p, style: p.style, previousStyle: p.previousStyle, + theme: p.theme, } child.setOptions(opt...) return child @@ -286,6 +312,22 @@ func (p *Painter) Fill() { p.render.Fill() } +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) SetStrokeColor(color Color) { + p.render.SetStrokeColor(color) +} + func (p *Painter) LineStroke(points []Point, style LineStyle) { s := style.Style() if !s.ShouldDrawStroke() { diff --git a/painer_test.go b/painter_test.go similarity index 100% rename from painer_test.go rename to painter_test.go diff --git a/util.go b/util.go index c895cc3..d35b4b0 100644 --- a/util.go +++ b/util.go @@ -76,11 +76,11 @@ func autoDivide(max, size int) []int { } // measureTextMaxWidthHeight returns maxWidth and maxHeight of text list -func measureTextMaxWidthHeight(textList []string, r chart.Renderer) (int, int) { +func measureTextMaxWidthHeight(textList []string, p *Painter) (int, int) { maxWidth := 0 maxHeight := 0 for _, text := range textList { - box := r.MeasureText(text) + box := p.MeasureText(text) maxWidth = chart.MaxInt(maxWidth, box.Width()) maxHeight = chart.MaxInt(maxHeight, box.Height()) } diff --git a/xaxis.go b/xaxis.go index edd017f..d79f40e 100644 --- a/xaxis.go +++ b/xaxis.go @@ -24,7 +24,6 @@ package charts import ( "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2" ) type XAxisOption struct { @@ -53,27 +52,19 @@ func NewXAxisOption(data []string, boundaryGap ...*bool) XAxisOption { } // drawXAxis draws x axis, and returns the height, range of if. -func drawXAxis(p *Draw, opt *XAxisOption, yAxisCount int) (int, *Range, error) { +func drawXAxis(p *Painter, opt *XAxisOption, yAxisCount int) (int, *Range, error) { if opt.Hidden { return 0, nil, nil } left := YAxisWidth right := (yAxisCount - 1) * YAxisWidth - dXAxis, err := NewDraw( - DrawOption{ - Parent: p, - }, - PaddingOption(chart.Box{ + pXAxis := p.Child( + PainterPaddingOption(Box{ Left: left, Right: right, }), + PainterFontOption(opt.Font), ) - if opt.Font != nil { - dXAxis.Font = opt.Font - } - if err != nil { - return 0, nil, err - } theme := NewTheme(opt.Theme) data := NewAxisDataListFromStringList(opt.Data) style := AxisOption{ @@ -90,13 +81,13 @@ func drawXAxis(p *Draw, opt *XAxisOption, yAxisCount int) (int, *Range, error) { boundary = false max-- } - axis := NewAxis(dXAxis, data, style) + axis := NewAxis(pXAxis, data, style) axis.Render() return axis.measure().Height, &Range{ divideCount: len(opt.Data), Min: 0, Max: max, - Size: dXAxis.Box.Width(), + Size: pXAxis.Width(), Boundary: boundary, }, nil } diff --git a/yaxis.go b/yaxis.go index a14e409..5d55440 100644 --- a/yaxis.go +++ b/yaxis.go @@ -45,7 +45,7 @@ type YAxisOption struct { // TODO 长度是否可以变化 const YAxisWidth = 40 -func drawYAxis(p *Draw, opt *ChartOption, axisIndex, xAxisHeight int, padding chart.Box) (*Range, error) { +func drawYAxis(p *Painter, opt *ChartOption, axisIndex, xAxisHeight int, padding chart.Box) (*Range, error) { theme := NewTheme(opt.Theme) yRange := opt.newYRange(axisIndex) values := yRange.Values() @@ -74,32 +74,22 @@ func drawYAxis(p *Draw, opt *ChartOption, axisIndex, xAxisHeight int, padding ch width := NewAxis(p, data, style).measure().Width yAxisCount := len(opt.YAxisList) - boxWidth := p.Box.Width() + boxWidth := p.Width() if axisIndex > 0 { style.SplitLineShow = false style.Position = PositionRight padding.Right += (axisIndex - 1) * YAxisWidth } else { - boxWidth = p.Box.Width() - (yAxisCount-1)*YAxisWidth + boxWidth = p.Width() - (yAxisCount-1)*YAxisWidth padding.Left += (YAxisWidth - width) } - dYAxis, err := NewDraw( - DrawOption{ - Parent: p, - Width: boxWidth, - // 减去x轴的高 - Height: p.Box.Height() - xAxisHeight, - }, - PaddingOption(padding), + pYAxis := p.Child( + PainterWidthHeightOption(boxWidth, p.Height()-xAxisHeight), + PainterPaddingOption(padding), + PainterFontOption(opt.Font), ) - if err != nil { - return nil, err - } - if opt.Font != nil { - dYAxis.Font = opt.Font - } - NewAxis(dYAxis, data, style).Render() - yRange.Size = dYAxis.Box.Height() + NewAxis(pYAxis, data, style).Render() + yRange.Size = pYAxis.Height() return &yRange, nil } From c363d1d5e37e08d01ddee82160ff5a0d4abded71 Mon Sep 17 00:00:00 2001 From: vicanso Date: Mon, 16 May 2022 20:58:41 +0800 Subject: [PATCH 003/116] refactor: reset --- alias.go | 31 + axis.go | 472 ---------- axis_test.go | 259 ------ bar.go | 58 -- bar_chart.go | 163 ---- bar_chart_test.go | 131 --- bar_test.go | 78 -- chart.go | 502 ----------- chart_option.go | 190 ---- chart_option_test.go | 238 ----- chart_test.go | 567 ------------ draw.go | 372 -------- draw_test.go | 507 ----------- echarts.go | 499 ----------- echarts_test.go | 592 ------------- examples/basic/main.go | 94 -- examples/charts/main.go | 1809 --------------------------------------- funnel.go | 141 --- funnel_test.go | 91 -- legend.go | 226 ----- legend_test.go | 185 ---- line.go | 60 +- line_chart.go | 131 --- line_chart_test.go | 97 --- line_test.go | 165 ---- mark_line.go | 92 -- mark_line_test.go | 99 --- mark_point.go | 89 -- mark_point_test.go | 103 --- painter.go | 8 + painter_test.go | 2 +- pie_chart.go | 159 ---- pie_chart_test.go | 69 -- radar_chart.go | 193 ----- radar_chart_test.go | 102 --- range.go | 109 --- range_test.go | 94 -- series.go | 233 ----- series_test.go | 166 ---- table.go | 145 ---- theme.go | 203 +---- theme_test.go | 87 -- title.go | 155 ---- title_test.go | 142 --- util.go | 4 +- util_test.go | 10 +- xaxis.go | 93 -- xaxis_test.go | 108 --- yaxis.go | 95 -- yaxis_test.go | 119 --- 50 files changed, 55 insertions(+), 10282 deletions(-) delete mode 100644 axis.go delete mode 100644 axis_test.go delete mode 100644 bar.go delete mode 100644 bar_chart.go delete mode 100644 bar_chart_test.go delete mode 100644 bar_test.go delete mode 100644 chart.go delete mode 100644 chart_option.go delete mode 100644 chart_option_test.go delete mode 100644 chart_test.go delete mode 100644 draw.go delete mode 100644 draw_test.go delete mode 100644 echarts.go delete mode 100644 echarts_test.go delete mode 100644 examples/basic/main.go delete mode 100644 examples/charts/main.go delete mode 100644 funnel.go delete mode 100644 funnel_test.go delete mode 100644 legend.go delete mode 100644 legend_test.go delete mode 100644 line_chart.go delete mode 100644 line_chart_test.go delete mode 100644 line_test.go delete mode 100644 mark_line.go delete mode 100644 mark_line_test.go delete mode 100644 mark_point.go delete mode 100644 mark_point_test.go delete mode 100644 pie_chart.go delete mode 100644 pie_chart_test.go delete mode 100644 radar_chart.go delete mode 100644 radar_chart_test.go delete mode 100644 range.go delete mode 100644 range_test.go delete mode 100644 series.go delete mode 100644 series_test.go delete mode 100644 table.go delete mode 100644 theme_test.go delete mode 100644 title.go delete mode 100644 title_test.go delete mode 100644 xaxis.go delete mode 100644 xaxis_test.go delete mode 100644 yaxis.go delete mode 100644 yaxis_test.go diff --git a/alias.go b/alias.go index 3a09919..3bacc67 100644 --- a/alias.go +++ b/alias.go @@ -31,3 +31,34 @@ type Box = chart.Box type Renderer = chart.Renderer type Style = chart.Style type Color = drawing.Color + +type Point struct { + X int + Y int +} + +const ( + ChartTypeLine = "line" + ChartTypeBar = "bar" + ChartTypePie = "pie" + ChartTypeRadar = "radar" + ChartTypeFunnel = "funnel" +) + +const ( + ChartOutputSVG = "svg" + ChartOutputPNG = "png" +) + +const ( + PositionLeft = "left" + PositionRight = "right" + PositionCenter = "center" + PositionTop = "top" + PositionBottom = "bottom" +) + +const ( + OrientHorizontal = "horizontal" + OrientVertical = "vertical" +) diff --git a/axis.go b/axis.go deleted file mode 100644 index 5881f5e..0000000 --- a/axis.go +++ /dev/null @@ -1,472 +0,0 @@ -// 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" - - "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2" -) - -type AxisOption struct { - // The boundary gap on both sides of a coordinate axis. - // Nil or *true means the center part of two axis ticks - BoundaryGap *bool - // The flag for show axis, set this to *false will hide axis - Show *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 - ClassName string - // The line color of axis - StrokeColor Color - // The line width - StrokeWidth float64 - // The length of the axis tick - TickLength int - // The flag for show axis tick, set this to *false will hide axis tick - TickShow *bool - // 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 -} - -type axis struct { - painter *Painter - data *AxisDataList - option *AxisOption -} -type axisMeasurement struct { - Width int - Height int -} - -// NewAxis creates a new axis with data and style options -func NewAxis(p *Painter, data AxisDataList, option AxisOption) *axis { - return &axis{ - painter: p, - data: &data, - option: &option, - } - -} - -// GetLabelMargin returns the label margin value -func (as *AxisOption) GetLabelMargin() int { - return getDefaultInt(as.LabelMargin, 8) -} - -// GetTickLength returns the tick length value -func (as *AxisOption) GetTickLength() int { - return getDefaultInt(as.TickLength, 5) -} - -// Style returns the style of axis -func (as *AxisOption) Style(f *truetype.Font) chart.Style { - s := chart.Style{ - ClassName: as.ClassName, - StrokeColor: as.StrokeColor, - StrokeWidth: as.StrokeWidth, - FontSize: as.FontSize, - FontColor: as.FontColor, - Font: as.Font, - } - if s.FontSize == 0 { - s.FontSize = chart.DefaultFontSize - } - if s.Font == nil { - s.Font = f - } - return s -} - -type AxisData struct { - // The text value of axis - Text string -} -type AxisDataList []AxisData - -// TextList returns the text list of axis data -func (l AxisDataList) TextList() []string { - textList := make([]string, len(l)) - for index, item := range l { - textList[index] = item.Text - } - return textList -} - -type axisRenderOption struct { - textMaxWith int - textMaxHeight int - boundaryGap bool - unitCount int - modValue int -} - -// NewAxisDataListFromStringList creates a new axis data list from string list -func NewAxisDataListFromStringList(textList []string) AxisDataList { - list := make(AxisDataList, len(textList)) - for index, text := range textList { - list[index] = AxisData{ - Text: text, - } - } - return list -} - -func (a *axis) axisLabel(renderOpt *axisRenderOption) { - option := a.option - data := *a.data - // d := a.d - if option.FontColor.IsZero() || len(data) == 0 { - return - } - // r := d.Render - - // s.GetTextOptions().WriteTextOptionsToRenderer(r) - p := a.painter - s := option.Style(p.font) - p.SetTextStyle(s) - - width := p.Width() - height := p.Height() - textList := data.TextList() - count := len(textList) - - boundaryGap := renderOpt.boundaryGap - if !boundaryGap { - count-- - } - - unitCount := renderOpt.unitCount - modValue := renderOpt.modValue - labelMargin := option.GetLabelMargin() - - // 轴线 - labelHeight := labelMargin + renderOpt.textMaxHeight - labelWidth := labelMargin + renderOpt.textMaxWith - - // 坐标轴文本 - position := option.Position - switch position { - case PositionLeft: - fallthrough - case PositionRight: - values := autoDivide(height, count) - textList := data.TextList() - // 由下往上 - reverseIntSlice(values) - for index, text := range textList { - y := values[index] - 2 - b := p.MeasureText(text) - if boundaryGap { - height := y - values[index+1] - y -= (height - b.Height()) >> 1 - } else { - y += b.Height() >> 1 - } - // 左右位置的x不一样 - x := width - renderOpt.textMaxWith - if position == PositionLeft { - x = labelWidth - b.Width() - 1 - } - p.Text(text, x, y) - } - default: - // 定位bottom,重新计算y0的定位 - y0 := height - labelMargin - if position == PositionTop { - y0 = labelHeight - labelMargin - } - values := autoDivide(width, count) - for index, text := range data.TextList() { - if unitCount != 0 && index%unitCount != modValue { - continue - } - x := values[index] - leftOffset := 0 - b := p.MeasureText(text) - if boundaryGap { - width := values[index+1] - x - leftOffset = (width - b.Width()) >> 1 - } else { - // 左移文本长度 - leftOffset = -b.Width() >> 1 - } - p.Text(text, x+leftOffset, y0) - } - } -} - -func (a *axis) axisLine(renderOpt *axisRenderOption) { - // d := a.d - // r := d.Render - p := a.painter - option := a.option - s := option.Style(p.font) - p.SetDrawingStyle(s.GetStrokeOptions()) - - x0 := 0 - y0 := 0 - x1 := 0 - y1 := 0 - width := p.Width() - height := p.Height() - labelMargin := option.GetLabelMargin() - - // 轴线 - labelHeight := labelMargin + renderOpt.textMaxHeight - labelWidth := labelMargin + renderOpt.textMaxWith - tickLength := option.GetTickLength() - switch option.Position { - case PositionLeft: - x0 = tickLength + labelWidth - x1 = x0 - y0 = 0 - y1 = height - case PositionRight: - x0 = width - labelWidth - x1 = x0 - y0 = 0 - y1 = height - case PositionTop: - x0 = 0 - x1 = width - y0 = labelHeight - y1 = y0 - // bottom - default: - x0 = 0 - x1 = width - y0 = height - tickLength - labelHeight - y1 = y0 - } - - p.MoveTo(x0, y0) - p.LineTo(x1, y1) - p.FillStroke() -} - -func (a *axis) axisTick(renderOpt *axisRenderOption) { - // d := a.d - // r := d.Render - - p := a.painter - option := a.option - s := option.Style(p.font) - p.SetDrawingStyle(s.GetStrokeOptions()) - - width := p.Width() - height := p.Height() - data := *a.data - tickCount := len(data) - if tickCount == 0 { - return - } - if !renderOpt.boundaryGap { - tickCount-- - } - labelMargin := option.GetLabelMargin() - tickShow := true - if isFalse(option.TickShow) { - tickShow = false - } - unitCount := renderOpt.unitCount - - tickLengthValue := option.GetTickLength() - labelHeight := labelMargin + renderOpt.textMaxHeight - labelWidth := labelMargin + renderOpt.textMaxWith - position := option.Position - switch position { - case PositionLeft: - fallthrough - case PositionRight: - values := autoDivide(height, tickCount) - // 左右仅是x0的位置不一样 - x0 := width - labelWidth - if option.Position == PositionLeft { - x0 = labelWidth - } - if tickShow { - for _, v := range values { - x := x0 - y := v - p.MoveTo(x, y) - p.LineTo(x+tickLengthValue, y) - p.Stroke() - } - } - // 辅助线 - if option.SplitLineShow && !option.SplitLineColor.IsZero() { - p.SetStrokeColor(option.SplitLineColor) - splitLineWidth := width - labelWidth - tickLengthValue - x0 = labelWidth + tickLengthValue - if position == PositionRight { - x0 = 0 - splitLineWidth = width - labelWidth - 1 - } - for _, v := range values[0 : len(values)-1] { - x := x0 - y := v - p.MoveTo(x, y) - p.LineTo(x+splitLineWidth, y) - p.Stroke() - } - } - default: - values := autoDivide(width, tickCount) - // 上下仅是y0的位置不一样 - y0 := height - labelHeight - if position == PositionTop { - y0 = labelHeight - } - if tickShow { - for index, v := range values { - if index%unitCount != 0 { - continue - } - x := v - y := y0 - p.MoveTo(x, y-tickLengthValue) - p.LineTo(x, y) - p.Stroke() - } - } - // 辅助线 - if option.SplitLineShow && !option.SplitLineColor.IsZero() { - p.SetStrokeColor(option.SplitLineColor) - y0 = 0 - splitLineHeight := height - labelHeight - tickLengthValue - if position == PositionTop { - y0 = labelHeight - splitLineHeight = height - labelHeight - } - - for index, v := range values { - if index%unitCount != 0 { - continue - } - x := v - y := y0 - - p.MoveTo(x, y) - p.LineTo(x, y0+splitLineHeight) - p.Stroke() - } - } - } -} - -func (a *axis) measureTextMaxWidthHeight() (int, int) { - // d := a.d - // r := d.Render - p := a.painter - s := a.option.Style(p.font) - data := a.data - p.SetDrawingStyle(s.GetStrokeOptions()) - p.SetTextStyle(s.GetTextOptions()) - return measureTextMaxWidthHeight(data.TextList(), p) -} - -// measure returns the measurement of axis. -// Width will be textMaxWidth + labelMargin + tickLength for position left or right. -// Height will be textMaxHeight + labelMargin + tickLength for position top or bottom. -func (a *axis) measure() axisMeasurement { - option := a.option - value := option.GetLabelMargin() + option.GetTickLength() - textMaxWidth, textMaxHeight := a.measureTextMaxWidthHeight() - info := axisMeasurement{} - if option.Position == PositionLeft || - option.Position == PositionRight { - info.Width = textMaxWidth + value - } else { - info.Height = textMaxHeight + value - } - return info -} - -// Render renders the axis for chart -func (a *axis) Render() { - option := a.option - if isFalse(option.Show) { - return - } - textMaxWidth, textMaxHeight := a.measureTextMaxWidthHeight() - opt := &axisRenderOption{ - textMaxWith: textMaxWidth, - textMaxHeight: textMaxHeight, - boundaryGap: true, - } - if isFalse(option.BoundaryGap) { - opt.boundaryGap = false - } - - unitCount := chart.MaxInt(option.SplitNumber, 1) - width := a.painter.Width() - textList := a.data.TextList() - count := len(textList) - - position := option.Position - switch position { - case PositionLeft: - fallthrough - case PositionRight: - default: - maxCount := width / (opt.textMaxWith + 10) - // 可以显示所有 - if maxCount >= count { - unitCount = 1 - } else if maxCount < count/unitCount { - unitCount = int(math.Ceil(float64(count) / float64(maxCount))) - } - } - - boundaryGap := opt.boundaryGap - modValue := 0 - if boundaryGap && unitCount > 1 { - // 如果是居中,unit count需要设置为奇数 - if unitCount%2 == 0 { - unitCount++ - } - modValue = unitCount / 2 - } - opt.modValue = modValue - opt.unitCount = unitCount - - // 坐标轴线 - a.axisLine(opt) - a.axisTick(opt) - // 坐标文本 - a.axisLabel(opt) -} diff --git a/axis_test.go b/axis_test.go deleted file mode 100644 index fe253e9..0000000 --- a/axis_test.go +++ /dev/null @@ -1,259 +0,0 @@ -// 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/golang/freetype/truetype" - "github.com/stretchr/testify/assert" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" -) - -func TestAxisOption(t *testing.T) { - assert := assert.New(t) - - as := AxisOption{} - - assert.Equal(8, as.GetLabelMargin()) - as.LabelMargin = 10 - assert.Equal(10, as.GetLabelMargin()) - - assert.Equal(5, as.GetTickLength()) - as.TickLength = 6 - assert.Equal(6, as.GetTickLength()) - - f := &truetype.Font{} - style := as.Style(f) - assert.Equal(float64(10), style.FontSize) - assert.Equal(f, style.Font) -} - -func TestAxisDataList(t *testing.T) { - assert := assert.New(t) - - textList := []string{ - "a", - "b", - } - data := NewAxisDataListFromStringList(textList) - assert.Equal(textList, data.TextList()) -} - -func TestAxis(t *testing.T) { - assert := assert.New(t) - - axisData := NewAxisDataListFromStringList([]string{ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun", - }) - getDefaultOption := func() AxisOption { - return AxisOption{ - StrokeColor: drawing.ColorBlack, - StrokeWidth: 1, - FontColor: drawing.ColorBlack, - Show: TrueFlag(), - TickShow: TrueFlag(), - SplitLineShow: true, - SplitLineColor: drawing.ColorBlack.WithAlpha(60), - } - } - tests := []struct { - newOption func() AxisOption - newData func() AxisDataList - result string - }{ - // 文本按起始位置展示 - // axis位于bottom - { - newOption: func() AxisOption { - opt := getDefaultOption() - opt.BoundaryGap = FalseFlag() - return opt - }, - result: "\\nMonTueWedThuFriSatSun", - }, - // 文本居中展示 - // axis位于bottom - { - newOption: func() AxisOption { - opt := getDefaultOption() - return opt - }, - result: "\\nMonTueWedThuFriSatSun", - }, - // 文本按起始位置展示 - // axis位于top - { - newOption: func() AxisOption { - opt := getDefaultOption() - opt.Position = PositionTop - opt.BoundaryGap = FalseFlag() - return opt - }, - result: "\\nMonTueWedThuFriSatSun", - }, - // 文本居中展示 - // axis位于top - { - newOption: func() AxisOption { - opt := getDefaultOption() - opt.Position = PositionTop - return opt - }, - result: "\\nMonTueWedThuFriSatSun", - }, - // 文本按起始位置展示 - // axis位于left - { - newOption: func() AxisOption { - opt := getDefaultOption() - opt.Position = PositionLeft - opt.BoundaryGap = FalseFlag() - return opt - }, - result: "\\nMonTueWedThuFriSatSun", - }, - // 文本居中展示 - // axis位于left - { - newOption: func() AxisOption { - opt := getDefaultOption() - opt.Position = PositionLeft - return opt - }, - result: "\\nMonTueWedThuFriSatSun", - }, - // 文本按起始位置展示 - // axis位于right - { - newOption: func() AxisOption { - opt := getDefaultOption() - opt.Position = PositionRight - opt.BoundaryGap = FalseFlag() - return opt - }, - result: "\\nMonTueWedThuFriSatSun", - }, - // 文本居中展示 - // axis位于right - { - newOption: func() AxisOption { - opt := getDefaultOption() - opt.Position = PositionRight - return opt - }, - result: "\\nMonTueWedThuFriSatSun", - }, - // text较多,仅展示部分 - { - newOption: func() AxisOption { - opt := getDefaultOption() - opt.Position = PositionBottom - return opt - }, - newData: func() AxisDataList { - return NewAxisDataListFromStringList([]string{ - "01-01", - "01-02", - "01-03", - "01-04", - "01-05", - "01-06", - "01-07", - "01-08", - "01-09", - "01-10", - "01-11", - "01-12", - "01-13", - "01-14", - "01-15", - "01-16", - "01-17", - "01-18", - "01-19", - "01-20", - "01-21", - }) - }, - result: "\\n01-0201-0501-0801-1101-1401-1701-20", - }, - } - for _, tt := range tests { - p, err := NewPainter(PainterOptions{ - Width: 400, - Height: 300, - }, PainterPaddingOption(chart.Box{ - Left: 5, - Top: 5, - Right: 5, - Bottom: 5, - })) - assert.Nil(err) - style := tt.newOption() - data := axisData - if tt.newData != nil { - data = tt.newData() - } - NewAxis(p, data, style).Render() - - result, err := p.Bytes() - assert.Nil(err) - assert.Equal(tt.result, string(result)) - } -} - -func TestMeasureAxis(t *testing.T) { - assert := assert.New(t) - - p, err := NewPainter(PainterOptions{ - Width: 400, - Height: 300, - }) - assert.Nil(err) - data := NewAxisDataListFromStringList([]string{ - "Mon", - "Sun", - }) - f, _ := chart.GetDefaultFont() - width := NewAxis(p, data, AxisOption{ - FontSize: 12, - Font: f, - Position: PositionLeft, - }).measure().Width - assert.Equal(44, width) - - height := NewAxis(p, data, AxisOption{ - FontSize: 12, - Font: f, - Position: PositionTop, - }).measure().Height - assert.Equal(28, height) -} diff --git a/bar.go b/bar.go deleted file mode 100644 index 1090f6b..0000000 --- a/bar.go +++ /dev/null @@ -1,58 +0,0 @@ -// 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 BarStyle struct { - ClassName string - StrokeDashArray []float64 - FillColor drawing.Color -} - -func (bs *BarStyle) Style() chart.Style { - return chart.Style{ - ClassName: bs.ClassName, - StrokeDashArray: bs.StrokeDashArray, - StrokeColor: bs.FillColor, - StrokeWidth: 1, - FillColor: bs.FillColor, - } -} - -// Bar renders bar for chart -func (d *Draw) Bar(b chart.Box, style BarStyle) { - s := style.Style() - - r := d.Render - s.GetFillAndStrokeOptions().WriteToRenderer(r) - d.moveTo(b.Left, b.Top) - d.lineTo(b.Right, b.Top) - d.lineTo(b.Right, b.Bottom) - d.lineTo(b.Left, b.Bottom) - d.lineTo(b.Left, b.Top) - d.Render.FillStroke() -} diff --git a/bar_chart.go b/bar_chart.go deleted file mode 100644 index 32373b1..0000000 --- a/bar_chart.go +++ /dev/null @@ -1,163 +0,0 @@ -// 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" - "github.com/wcharczuk/go-chart/v2" -) - -type barChartOption struct { - // The series list fo bar chart - SeriesList SeriesList - // The theme - Theme string - // The font - Font *truetype.Font -} - -func barChartRender(opt barChartOption, result *basicRenderResult) ([]markPointRenderOption, error) { - d, err := NewDraw(DrawOption{ - Parent: result.d, - }, PaddingOption(chart.Box{ - Top: result.titleBox.Height(), - // TODO 后续考虑是否需要根据左侧是否展示y轴再生成对应的left - Left: YAxisWidth, - })) - if err != nil { - return nil, err - } - xRange := result.xRange - x0, x1 := xRange.GetRange(0) - width := int(x1 - x0) - // 每一块之间的margin - margin := 10 - // 每一个bar之间的margin - barMargin := 5 - if width < 20 { - margin = 2 - barMargin = 2 - } else if width < 50 { - margin = 5 - barMargin = 3 - } - - seriesCount := len(opt.SeriesList) - // 总的宽度-两个margin-(总数-1)的barMargin - barWidth := (width - 2*margin - barMargin*(seriesCount-1)) / len(opt.SeriesList) - - barMaxHeight := result.getYRange(0).Size - theme := NewTheme(opt.Theme) - - seriesNames := opt.SeriesList.Names() - - r := d.Render - - markPointRenderOptions := make([]markPointRenderOption, 0) - - for i, s := range opt.SeriesList { - // 由于series是for range,为同一个数据,因此需要clone - // 后续需要使用,如mark point - series := s - yRange := result.getYRange(series.YAxisIndex) - points := make([]Point, len(series.Data)) - index := series.index - if index == 0 { - index = i - } - seriesColor := theme.GetSeriesColor(index) - // mark line - markLineRender(markLineRenderOption{ - Draw: d, - FillColor: seriesColor, - FontColor: theme.GetTextColor(), - StrokeColor: seriesColor, - Font: opt.Font, - Series: &series, - Range: yRange, - }) - divideValues := xRange.AutoDivide() - for j, item := range series.Data { - if j >= xRange.divideCount { - continue - } - x := divideValues[j] - x += margin - if i != 0 { - x += i * (barWidth + barMargin) - } - - h := int(yRange.getHeight(item.Value)) - fillColor := seriesColor - if !item.Style.FillColor.IsZero() { - fillColor = item.Style.FillColor - } - top := barMaxHeight - h - d.Bar(chart.Box{ - Top: top, - Left: x, - Right: x + barWidth, - Bottom: barMaxHeight - 1, - }, BarStyle{ - FillColor: fillColor, - }) - // 用于生成marker point - points[j] = Point{ - // 居中的位置 - X: x + barWidth>>1, - Y: top, - } - // 如果label不需要展示,则返回 - if !series.Label.Show { - continue - } - distance := series.Label.Distance - if distance == 0 { - distance = 5 - } - text := NewValueLabelFormater(seriesNames, series.Label.Formatter)(i, item.Value, -1) - labelStyle := chart.Style{ - FontColor: theme.GetTextColor(), - FontSize: labelFontSize, - Font: opt.Font, - } - if !series.Label.Color.IsZero() { - labelStyle.FontColor = series.Label.Color - } - labelStyle.GetTextOptions().WriteToRenderer(r) - textBox := r.MeasureText(text) - d.text(text, x+(barWidth-textBox.Width())>>1, barMaxHeight-h-distance) - } - - // 生成mark point的参数 - markPointRenderOptions = append(markPointRenderOptions, markPointRenderOption{ - Draw: d, - FillColor: seriesColor, - Font: opt.Font, - Points: points, - Series: &series, - }) - } - - return markPointRenderOptions, nil -} diff --git a/bar_chart_test.go b/bar_chart_test.go deleted file mode 100644 index f10a1bc..0000000 --- a/bar_chart_test.go +++ /dev/null @@ -1,131 +0,0 @@ -// 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" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" -) - -func TestBarChartRender(t *testing.T) { - assert := assert.New(t) - - width := 400 - height := 300 - d, err := NewDraw(DrawOption{ - Width: width, - Height: height, - }) - assert.Nil(err) - - result := basicRenderResult{ - xRange: &Range{ - Min: 0, - Max: 4, - divideCount: 4, - Size: width, - Boundary: true, - }, - yRangeList: []*Range{ - { - divideCount: 6, - Max: 100, - Min: 0, - Size: height, - }, - }, - d: d, - } - f, _ := chart.GetDefaultFont() - - markPointOptions, err := barChartRender(barChartOption{ - Font: f, - SeriesList: SeriesList{ - { - Label: SeriesLabel{ - Show: true, - Color: drawing.ColorBlue, - }, - MarkLine: NewMarkLine( - SeriesMarkDataTypeMin, - ), - Data: []SeriesData{ - { - Value: 20, - }, - { - Value: 60, - Style: chart.Style{ - FillColor: drawing.ColorRed, - }, - }, - { - Value: 90, - }, - }, - }, - NewSeriesFromValues([]float64{ - 80, - 30, - 70, - }), - }, - }, &result) - assert.Nil(err) - assert.Equal(2, len(markPointOptions)) - assert.Equal([]Point{ - { - X: 28, - Y: 240, - }, - { - X: 128, - Y: 120, - }, - { - X: 228, - Y: 30, - }, - }, markPointOptions[0].Points) - assert.Equal([]Point{ - { - X: 70, - Y: 60, - }, - { - X: 170, - Y: 210, - }, - { - X: 270, - Y: 90, - }, - }, markPointOptions[1].Points) - - data, err := d.Bytes() - assert.Nil(err) - assert.Equal("\\n20206090", string(data)) -} diff --git a/bar_test.go b/bar_test.go deleted file mode 100644 index 01b6d3c..0000000 --- a/bar_test.go +++ /dev/null @@ -1,78 +0,0 @@ -// 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" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" -) - -func TestBarStyle(t *testing.T) { - assert := assert.New(t) - - bs := BarStyle{ - ClassName: "test", - StrokeDashArray: []float64{ - 1.0, - }, - FillColor: drawing.ColorBlack, - } - - assert.Equal(chart.Style{ - ClassName: "test", - StrokeDashArray: []float64{ - 1.0, - }, - StrokeWidth: 1, - FillColor: drawing.ColorBlack, - StrokeColor: drawing.ColorBlack, - }, bs.Style()) -} - -func TestDrawBar(t *testing.T) { - assert := assert.New(t) - d, err := NewDraw(DrawOption{ - Width: 400, - Height: 300, - }, PaddingOption(chart.Box{ - Left: 10, - Top: 20, - Right: 30, - Bottom: 40, - })) - assert.Nil(err) - d.Bar(chart.Box{ - Left: 0, - Top: 0, - Right: 20, - Bottom: 200, - }, BarStyle{ - FillColor: drawing.ColorBlack, - }) - data, err := d.Bytes() - assert.Nil(err) - assert.Equal("\\n", string(data)) -} diff --git a/chart.go b/chart.go deleted file mode 100644 index 21f2071..0000000 --- a/chart.go +++ /dev/null @@ -1,502 +0,0 @@ -// 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 ( - "errors" - "math" - "sort" - "strings" - - "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" -) - -const ( - ChartTypeLine = "line" - ChartTypeBar = "bar" - ChartTypePie = "pie" - ChartTypeRadar = "radar" - ChartTypeFunnel = "funnel" -) - -const ( - ChartOutputSVG = "svg" - ChartOutputPNG = "png" -) - -type Point struct { - X int - Y int -} - -const labelFontSize = 10 -const defaultDotWidth = 2.0 -const defaultStrokeWidth = 2.0 - -var defaultChartWidth = 600 -var defaultChartHeight = 400 - -type ChartOption struct { - // The output type of chart, "svg" or "png", default value is "svg" - Type string - // The font family, which should be installed first - FontFamily string - // The font of chart, the default font is "roboto" - Font *truetype.Font - // The theme of chart, "light" and "dark". - // The default theme is "light" - Theme string - // The title option - Title TitleOption - // The legend option - Legend LegendOption - // The x axis option - XAxis XAxisOption - // The y axis option list - YAxisList []YAxisOption - // The width of chart, default width is 600 - Width int - // The height of chart, default height is 400 - Height int - Parent *Draw - // The padding for chart, default padding is [20, 10, 10, 10] - Padding chart.Box - // The canvas box for chart - Box chart.Box - // The series list - SeriesList SeriesList - // The radar indicator list - RadarIndicators []RadarIndicator - // The background color of chart - BackgroundColor drawing.Color - // The child charts - Children []ChartOption -} - -// FillDefault fills the default value for chart option -func (o *ChartOption) FillDefault(theme string) { - t := NewTheme(theme) - // 如果为空,初始化 - yAxisCount := 1 - for _, series := range o.SeriesList { - if series.YAxisIndex >= yAxisCount { - yAxisCount++ - } - } - yAxisList := make([]YAxisOption, yAxisCount) - copy(yAxisList, o.YAxisList) - o.YAxisList = yAxisList - - if o.Font == nil { - o.Font, _ = chart.GetDefaultFont() - } - if o.BackgroundColor.IsZero() { - o.BackgroundColor = t.GetBackgroundColor() - } - if o.Padding.IsZero() { - o.Padding = chart.Box{ - Top: 10, - Right: 10, - Bottom: 10, - Left: 10, - } - } - - // 标题的默认值 - if o.Title.Style.FontColor.IsZero() { - o.Title.Style.FontColor = t.GetTextColor() - } - if o.Title.Style.FontSize == 0 { - o.Title.Style.FontSize = 14 - } - if o.Title.Style.Font == nil { - o.Title.Style.Font = o.Font - } - if o.Title.Style.Padding.IsZero() { - o.Title.Style.Padding = chart.Box{ - Bottom: 10, - } - } - // 副标题 - if o.Title.SubtextStyle.FontColor.IsZero() { - o.Title.SubtextStyle.FontColor = o.Title.Style.FontColor.WithAlpha(180) - } - if o.Title.SubtextStyle.FontSize == 0 { - o.Title.SubtextStyle.FontSize = labelFontSize - } - if o.Title.SubtextStyle.Font == nil { - o.Title.SubtextStyle.Font = o.Font - } - - o.Legend.theme = theme - if o.Legend.Style.FontSize == 0 { - o.Legend.Style.FontSize = labelFontSize - } - if o.Legend.Left == "" { - o.Legend.Left = PositionCenter - } - // legend与series name的关联 - if len(o.Legend.Data) == 0 { - o.Legend.Data = o.SeriesList.Names() - } else { - seriesCount := len(o.SeriesList) - for index, name := range o.Legend.Data { - if index < seriesCount && - len(o.SeriesList[index].Name) == 0 { - o.SeriesList[index].Name = name - } - } - nameIndexDict := map[string]int{} - for index, name := range o.Legend.Data { - nameIndexDict[name] = index - } - // 保证series的顺序与legend一致 - sort.Slice(o.SeriesList, func(i, j int) bool { - return nameIndexDict[o.SeriesList[i].Name] < nameIndexDict[o.SeriesList[j].Name] - }) - } - // 如果无legend数据,则隐藏 - if len(strings.Join(o.Legend.Data, "")) == 0 { - o.Legend.Show = FalseFlag() - } - if o.Legend.Style.Font == nil { - o.Legend.Style.Font = o.Font - } - if o.Legend.Style.FontColor.IsZero() { - o.Legend.Style.FontColor = t.GetTextColor() - } - if o.XAxis.Theme == "" { - o.XAxis.Theme = theme - } - o.XAxis.Font = o.Font -} - -func (o *ChartOption) getWidth() int { - if o.Width != 0 { - return o.Width - } - if o.Parent != nil { - return o.Parent.Box.Width() - } - return defaultChartWidth -} - -func SetDefaultWidth(width int) { - if width > 0 { - defaultChartWidth = width - } -} -func SetDefaultHeight(height int) { - if height > 0 { - defaultChartHeight = height - } -} - -func (o *ChartOption) getHeight() int { - - if o.Height != 0 { - return o.Height - } - if o.Parent != nil { - return o.Parent.Box.Height() - } - return defaultChartHeight -} - -func (o *ChartOption) newYRange(axisIndex int) Range { - min := math.MaxFloat64 - max := -math.MaxFloat64 - if axisIndex >= len(o.YAxisList) { - axisIndex = 0 - } - yAxis := o.YAxisList[axisIndex] - - for _, series := range o.SeriesList { - if series.YAxisIndex != axisIndex { - continue - } - for _, item := range series.Data { - if item.Value > max { - max = item.Value - } - if item.Value < min { - min = item.Value - } - } - } - min = min * 0.9 - max = max * 1.1 - if yAxis.Min != nil { - min = *yAxis.Min - } - if yAxis.Max != nil { - max = *yAxis.Max - } - divideCount := 6 - // y轴分设置默认划分为6块 - r := NewRange(min, max, divideCount) - - // 由于NewRange会重新计算min max - if yAxis.Min != nil { - r.Min = min - } - if yAxis.Max != nil { - r.Max = max - } - - return r -} - -type basicRenderResult struct { - xRange *Range - yRangeList []*Range - d *Draw - titleBox chart.Box -} - -func (r *basicRenderResult) getYRange(index int) *Range { - if index >= len(r.yRangeList) { - index = 0 - } - return r.yRangeList[index] -} - -// Render renders the chart by option -func Render(opt ChartOption, optFuncs ...OptionFunc) (*Draw, error) { - for _, optFunc := range optFuncs { - optFunc(&opt) - } - if len(opt.SeriesList) == 0 { - return nil, errors.New("series can not be nil") - } - if len(opt.FontFamily) != 0 { - f, err := GetFont(opt.FontFamily) - if err != nil { - return nil, err - } - opt.Font = f - } - opt.FillDefault(opt.Theme) - - lineSeries := make([]Series, 0) - barSeries := make([]Series, 0) - isPieChart := false - isRadarChart := false - isFunnelChart := false - for index := range opt.SeriesList { - opt.SeriesList[index].index = index - item := opt.SeriesList[index] - switch item.Type { - case ChartTypePie: - isPieChart = true - case ChartTypeRadar: - isRadarChart = true - case ChartTypeFunnel: - isFunnelChart = true - case ChartTypeBar: - barSeries = append(barSeries, item) - default: - lineSeries = append(lineSeries, item) - } - } - // 如果指定了pie,则以pie的形式处理,pie不支持多类型图表 - // pie不需要axis - // radar 同样处理 - if isPieChart || - isRadarChart || - isFunnelChart { - opt.XAxis.Hidden = true - for index := range opt.YAxisList { - opt.YAxisList[index].Hidden = true - } - } - result, err := chartBasicRender(&opt) - if err != nil { - return nil, err - } - markPointRenderOptions := make([]markPointRenderOption, 0) - fns := []func() error{ - // pie render - func() error { - if !isPieChart { - return nil - } - return pieChartRender(pieChartOption{ - SeriesList: opt.SeriesList, - Theme: opt.Theme, - Font: opt.Font, - }, result) - }, - // radar render - func() error { - if !isRadarChart { - return nil - } - return radarChartRender(radarChartOption{ - SeriesList: opt.SeriesList, - Theme: opt.Theme, - Font: opt.Font, - Indicators: opt.RadarIndicators, - }, result) - }, - // funnel render - func() error { - if !isFunnelChart { - return nil - } - return funnelChartRender(funnelChartOption{ - SeriesList: opt.SeriesList, - Theme: opt.Theme, - Font: opt.Font, - }, result) - }, - // bar render - func() error { - // 如果无bar类型的series - if len(barSeries) == 0 { - return nil - } - options, err := barChartRender(barChartOption{ - SeriesList: barSeries, - Theme: opt.Theme, - Font: opt.Font, - }, result) - if err != nil { - return err - } - markPointRenderOptions = append(markPointRenderOptions, options...) - return nil - }, - // line render - func() error { - // 如果无line类型的series - if len(lineSeries) == 0 { - return nil - } - options, err := lineChartRender(lineChartOption{ - Theme: opt.Theme, - SeriesList: lineSeries, - Font: opt.Font, - }, result) - if err != nil { - return err - } - markPointRenderOptions = append(markPointRenderOptions, options...) - return nil - }, - // legend需要在顶层,因此此处render - func() error { - _, err := NewLegend(result.d, opt.Legend).Render() - return err - }, - // mark point最后render - func() error { - // mark point render不会出错 - for _, opt := range markPointRenderOptions { - markPointRender(opt) - } - return nil - }, - } - - for _, fn := range fns { - err = fn() - if err != nil { - return nil, err - } - } - for _, child := range opt.Children { - child.Parent = result.d - if len(child.Theme) == 0 { - child.Theme = opt.Theme - } - _, err = Render(child) - if err != nil { - return nil, err - } - } - return result.d, nil -} - -func chartBasicRender(opt *ChartOption) (*basicRenderResult, error) { - d, err := NewDraw( - DrawOption{ - Type: opt.Type, - Parent: opt.Parent, - Width: opt.getWidth(), - Height: opt.getHeight(), - }, - BoxOption(opt.Box), - PaddingOption(opt.Padding), - ) - if err != nil { - return nil, err - } - - if len(opt.YAxisList) > 2 { - return nil, errors.New("y axis should not be gt 2") - } - if opt.Parent == nil { - d.setBackground(opt.getWidth(), opt.getHeight(), opt.BackgroundColor) - } - - // 标题 - titleBox, err := drawTitle(d, &opt.Title) - if err != nil { - return nil, err - } - - xAxisHeight := 0 - var xRange *Range - - if !opt.XAxis.Hidden { - // xAxis - xAxisHeight, xRange, err = drawXAxis(d, &opt.XAxis, len(opt.YAxisList)) - if err != nil { - return nil, err - } - } - - yRangeList := make([]*Range, len(opt.YAxisList)) - - for index, yAxis := range opt.YAxisList { - var yRange *Range - if !yAxis.Hidden { - yRange, err = drawYAxis(d, opt, index, xAxisHeight, chart.Box{ - Top: titleBox.Height(), - }) - if err != nil { - return nil, err - } - yRangeList[index] = yRange - } - } - return &basicRenderResult{ - xRange: xRange, - yRangeList: yRangeList, - d: d, - titleBox: titleBox, - }, nil -} diff --git a/chart_option.go b/chart_option.go deleted file mode 100644 index 5e25873..0000000 --- a/chart_option.go +++ /dev/null @@ -1,190 +0,0 @@ -// 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" -) - -// OptionFunc option function -type OptionFunc func(opt *ChartOption) - -// PNGTypeOption set png type of chart's output -func PNGTypeOption() OptionFunc { - return TypeOptionFunc(ChartOutputPNG) -} - -// TypeOptionFunc set type of chart's output -func TypeOptionFunc(t string) OptionFunc { - return func(opt *ChartOption) { - opt.Type = t - } -} - -// FontFamilyOptionFunc set font family of chart -func FontFamilyOptionFunc(fontFamily string) OptionFunc { - return func(opt *ChartOption) { - opt.FontFamily = fontFamily - } -} - -// ThemeOptionFunc set them of chart -func ThemeOptionFunc(theme string) OptionFunc { - return func(opt *ChartOption) { - opt.Theme = theme - } -} - -// TitleOptionFunc set title of chart -func TitleOptionFunc(title TitleOption) OptionFunc { - return func(opt *ChartOption) { - opt.Title = title - } -} - -// LegendOptionFunc set legend of chart -func LegendOptionFunc(legend LegendOption) OptionFunc { - return func(opt *ChartOption) { - opt.Legend = legend - } -} - -// XAxisOptionFunc set x axis of chart -func XAxisOptionFunc(xAxisOption XAxisOption) OptionFunc { - return func(opt *ChartOption) { - opt.XAxis = xAxisOption - } -} - -// YAxisOptionFunc set y axis of chart, support two y axis -func YAxisOptionFunc(yAxisOption ...YAxisOption) OptionFunc { - return func(opt *ChartOption) { - opt.YAxisList = yAxisOption - } -} - -// WidthOptionFunc set width of chart -func WidthOptionFunc(width int) OptionFunc { - return func(opt *ChartOption) { - opt.Width = width - } -} - -// HeightOptionFunc set height of chart -func HeightOptionFunc(height int) OptionFunc { - return func(opt *ChartOption) { - opt.Height = height - } -} - -// PaddingOptionFunc set padding of chart -func PaddingOptionFunc(padding chart.Box) OptionFunc { - return func(opt *ChartOption) { - opt.Padding = padding - } -} - -// BoxOptionFunc set box of chart -func BoxOptionFunc(box chart.Box) OptionFunc { - return func(opt *ChartOption) { - opt.Box = box - } -} - -// ChildOptionFunc add child chart -func ChildOptionFunc(child ...ChartOption) OptionFunc { - return func(opt *ChartOption) { - if opt.Children == nil { - opt.Children = make([]ChartOption, 0) - } - opt.Children = append(opt.Children, child...) - } -} - -// RadarIndicatorOptionFunc set radar indicator of chart -func RadarIndicatorOptionFunc(radarIndicator ...RadarIndicator) OptionFunc { - return func(opt *ChartOption) { - opt.RadarIndicators = radarIndicator - } -} - -// BackgroundColorOptionFunc set background color of chart -func BackgroundColorOptionFunc(color drawing.Color) OptionFunc { - return func(opt *ChartOption) { - opt.BackgroundColor = color - } -} - -// LineRender line chart render -func LineRender(values [][]float64, opts ...OptionFunc) (*Draw, error) { - seriesList := make(SeriesList, len(values)) - for index, value := range values { - seriesList[index] = NewSeriesFromValues(value, ChartTypeLine) - } - return Render(ChartOption{ - SeriesList: seriesList, - }, opts...) -} - -// BarRender bar chart render -func BarRender(values [][]float64, opts ...OptionFunc) (*Draw, error) { - seriesList := make(SeriesList, len(values)) - for index, value := range values { - seriesList[index] = NewSeriesFromValues(value, ChartTypeBar) - } - return Render(ChartOption{ - SeriesList: seriesList, - }, opts...) -} - -// PieRender pie chart render -func PieRender(values []float64, opts ...OptionFunc) (*Draw, error) { - return Render(ChartOption{ - SeriesList: NewPieSeriesList(values), - }, opts...) -} - -// RadarRender radar chart render -func RadarRender(values [][]float64, opts ...OptionFunc) (*Draw, error) { - seriesList := make(SeriesList, len(values)) - for index, value := range values { - seriesList[index] = NewSeriesFromValues(value, ChartTypeRadar) - } - return Render(ChartOption{ - SeriesList: seriesList, - }, opts...) -} - -// FunnelRender funnel chart render -func FunnelRender(values []float64, opts ...OptionFunc) (*Draw, error) { - seriesList := make(SeriesList, len(values)) - for index, value := range values { - seriesList[index] = NewSeriesFromValues([]float64{ - value, - }, ChartTypeFunnel) - } - return Render(ChartOption{ - SeriesList: seriesList, - }, opts...) -} diff --git a/chart_option_test.go b/chart_option_test.go deleted file mode 100644 index 41e8d50..0000000 --- a/chart_option_test.go +++ /dev/null @@ -1,238 +0,0 @@ -// 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" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" -) - -func TestOptionFunc(t *testing.T) { - assert := assert.New(t) - - fns := []OptionFunc{ - TypeOptionFunc(ChartOutputPNG), - FontFamilyOptionFunc("fontFamily"), - ThemeOptionFunc("black"), - TitleOptionFunc(TitleOption{ - Text: "title", - }), - LegendOptionFunc(LegendOption{ - Data: []string{ - "a", - "b", - }, - }), - XAxisOptionFunc(NewXAxisOption([]string{ - "Mon", - "Tue", - })), - YAxisOptionFunc(YAxisOption{ - Min: NewFloatPoint(0), - Max: NewFloatPoint(100), - }), - WidthOptionFunc(400), - HeightOptionFunc(300), - PaddingOptionFunc(chart.Box{ - Top: 10, - }), - BoxOptionFunc(chart.Box{ - Left: 0, - Right: 300, - }), - ChildOptionFunc(ChartOption{}), - RadarIndicatorOptionFunc(RadarIndicator{ - Min: 0, - Max: 10, - }), - BackgroundColorOptionFunc(drawing.ColorBlack), - } - - opt := ChartOption{} - for _, fn := range fns { - fn(&opt) - } - - assert.Equal("png", opt.Type) - assert.Equal("fontFamily", opt.FontFamily) - assert.Equal("black", opt.Theme) - assert.Equal(TitleOption{ - Text: "title", - }, opt.Title) - assert.Equal(LegendOption{ - Data: []string{ - "a", - "b", - }, - }, opt.Legend) - assert.Equal(NewXAxisOption([]string{ - "Mon", - "Tue", - }), opt.XAxis) - assert.Equal([]YAxisOption{ - { - Min: NewFloatPoint(0), - Max: NewFloatPoint(100), - }, - }, opt.YAxisList) - assert.Equal(400, opt.Width) - assert.Equal(300, opt.Height) - assert.Equal(chart.Box{ - Top: 10, - }, opt.Padding) - assert.Equal(chart.Box{ - Left: 0, - Right: 300, - }, opt.Box) - assert.Equal(1, len(opt.Children)) - assert.Equal([]RadarIndicator{ - { - Min: 0, - Max: 10, - }, - }, opt.RadarIndicators) - assert.Equal(drawing.ColorBlack, opt.BackgroundColor) -} - -func TestLineRender(t *testing.T) { - assert := assert.New(t) - - d, err := LineRender([][]float64{ - { - 1, - 2, - 3, - }, - { - 1, - 5, - 2, - }, - }, - XAxisOptionFunc(NewXAxisOption([]string{ - "01", - "02", - "03", - })), - ) - assert.Nil(err) - data, err := d.Bytes() - assert.Nil(err) - assert.Equal("\\n010203024681012", string(data)) -} - -func TestBarRender(t *testing.T) { - assert := assert.New(t) - - d, err := BarRender([][]float64{ - { - 1, - 2, - 3, - }, - { - 1, - 5, - 2, - }, - }, - XAxisOptionFunc(NewXAxisOption([]string{ - "01", - "02", - "03", - })), - ) - assert.Nil(err) - data, err := d.Bytes() - assert.Nil(err) - assert.Equal("\\n010203024681012", string(data)) -} - -func TestPieRender(t *testing.T) { - assert := assert.New(t) - - d, err := PieRender([]float64{ - 1, - 3, - 5, - }) - assert.Nil(err) - data, err := d.Bytes() - assert.Nil(err) - assert.Equal("\\n", string(data)) -} - -func TestRadarRender(t *testing.T) { - assert := assert.New(t) - d, err := RadarRender([][]float64{ - { - 1, - 2, - 3, - }, - { - 1, - 5, - 2, - }, - }, - RadarIndicatorOptionFunc([]RadarIndicator{ - { - Name: "A", - Min: 0, - Max: 10, - }, - { - Name: "B", - Min: 0, - Max: 10, - }, - { - Name: "C", - Min: 0, - Max: 10, - }, - }...), - ) - assert.Nil(err) - data, err := d.Bytes() - assert.Nil(err) - assert.Equal("\\nABC", string(data)) -} - -func TestFunnelRender(t *testing.T) { - assert := assert.New(t) - - d, err := FunnelRender([]float64{ - 5, - 3, - 1, - }) - assert.Nil(err) - data, err := d.Bytes() - assert.Nil(err) - assert.Equal("\\n(100%)(60%)(20%)", string(data)) -} diff --git a/chart_test.go b/chart_test.go deleted file mode 100644 index c73745e..0000000 --- a/chart_test.go +++ /dev/null @@ -1,567 +0,0 @@ -// 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 ( - "errors" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" -) - -func TestChartSetDefaultWidthHeight(t *testing.T) { - assert := assert.New(t) - - width := defaultChartWidth - height := defaultChartHeight - defer SetDefaultWidth(width) - defer SetDefaultHeight(height) - - SetDefaultWidth(60) - assert.Equal(60, defaultChartWidth) - SetDefaultHeight(40) - assert.Equal(40, defaultChartHeight) -} - -func TestChartFillDefault(t *testing.T) { - assert := assert.New(t) - // default value - opt := ChartOption{} - opt.FillDefault("") - // padding - assert.Equal(chart.Box{ - Top: 10, - Right: 10, - Bottom: 10, - Left: 10, - }, opt.Padding) - // background color - assert.Equal(drawing.ColorWhite, opt.BackgroundColor) - // title font color - assert.Equal(drawing.Color{ - R: 70, - G: 70, - B: 70, - A: 255, - }, opt.Title.Style.FontColor) - // title font size - assert.Equal(float64(14), opt.Title.Style.FontSize) - // sub title font color - assert.Equal(drawing.Color{ - R: 70, - G: 70, - B: 70, - A: 180, - }, opt.Title.SubtextStyle.FontColor) - // sub title font size - assert.Equal(float64(10), opt.Title.SubtextStyle.FontSize) - // legend font size - assert.Equal(float64(10), opt.Legend.Style.FontSize) - // legend position - assert.Equal("center", opt.Legend.Left) - assert.Equal(drawing.Color{ - R: 70, - G: 70, - B: 70, - A: 255, - }, opt.Legend.Style.FontColor) - - // y axis - opt = ChartOption{ - SeriesList: SeriesList{ - { - YAxisIndex: 1, - }, - }, - } - opt.FillDefault("") - assert.Equal([]YAxisOption{ - {}, - {}, - }, opt.YAxisList) - opt = ChartOption{} - opt.FillDefault("") - assert.Equal([]YAxisOption{ - {}, - }, opt.YAxisList) - - // legend get from series's name - - opt = ChartOption{ - SeriesList: SeriesList{ - { - Name: "a", - }, - { - Name: "b", - }, - }, - } - opt.FillDefault("") - assert.Equal([]string{ - "a", - "b", - }, opt.Legend.Data) - // series name set by legend - opt = ChartOption{ - Legend: LegendOption{ - Data: []string{ - "a", - "b", - }, - }, - SeriesList: SeriesList{ - {}, - {}, - }, - } - opt.FillDefault("") - assert.Equal("a", opt.SeriesList[0].Name) - assert.Equal("b", opt.SeriesList[1].Name) -} - -func TestChartGetWidthHeight(t *testing.T) { - assert := assert.New(t) - - opt := ChartOption{ - Width: 10, - } - assert.Equal(10, opt.getWidth()) - opt.Width = 0 - assert.Equal(600, opt.getWidth()) - opt.Parent = &Draw{ - Box: chart.Box{ - Left: 10, - Right: 50, - }, - } - assert.Equal(40, opt.getWidth()) - - opt = ChartOption{ - Height: 20, - } - assert.Equal(20, opt.getHeight()) - opt.Height = 0 - assert.Equal(400, opt.getHeight()) - opt.Parent = &Draw{ - Box: chart.Box{ - Top: 20, - Bottom: 80, - }, - } - assert.Equal(60, opt.getHeight()) -} - -func TestChartRender(t *testing.T) { - assert := assert.New(t) - - d, err := Render(ChartOption{ - Width: 800, - Height: 600, - Legend: LegendOption{ - Top: "-90", - Data: []string{ - "Milk Tea", - "Matcha Latte", - "Cheese Cocoa", - "Walnut Brownie", - }, - }, - Padding: chart.Box{ - Top: 100, - }, - XAxis: NewXAxisOption([]string{ - "2012", - "2013", - "2014", - "2015", - "2016", - "2017", - }), - YAxisList: []YAxisOption{ - { - - Min: NewFloatPoint(0), - Max: NewFloatPoint(90), - }, - }, - SeriesList: []Series{ - NewSeriesFromValues([]float64{ - 56.5, - 82.1, - 88.7, - 70.1, - 53.4, - 85.1, - }), - NewSeriesFromValues([]float64{ - 51.1, - 51.4, - 55.1, - 53.3, - 73.8, - 68.7, - }), - NewSeriesFromValues([]float64{ - 40.1, - 62.2, - 69.5, - 36.4, - 45.2, - 32.5, - }, ChartTypeBar), - NewSeriesFromValues([]float64{ - 25.2, - 37.1, - 41.2, - 18, - 33.9, - 49.1, - }, ChartTypeBar), - }, - Children: []ChartOption{ - { - Legend: LegendOption{ - Show: FalseFlag(), - Data: []string{ - "Milk Tea", - "Matcha Latte", - "Cheese Cocoa", - "Walnut Brownie", - }, - }, - Box: chart.Box{ - Top: 20, - Left: 400, - Right: 500, - Bottom: 120, - }, - SeriesList: NewPieSeriesList([]float64{ - 435.9, - 354.3, - 285.9, - 204.5, - }, PieSeriesOption{ - Label: SeriesLabel{ - Show: true, - }, - Radius: "35%", - }), - }, - { - Legend: NewLegendOption([]string{ - "Allocated Budget", - "Actual Spending", - }), - Box: chart.Box{ - Top: 20, - Left: 0, - Right: 200, - Bottom: 120, - }, - RadarIndicators: []RadarIndicator{ - { - Name: "Sales", - Max: 6500, - }, - { - Name: "Administration", - Max: 16000, - }, - { - Name: "Information Technology", - Max: 30000, - }, - { - Name: "Customer Support", - Max: 38000, - }, - { - Name: "Development", - Max: 52000, - }, - { - Name: "Marketing", - Max: 25000, - }, - }, - SeriesList: SeriesList{ - { - Type: ChartTypeRadar, - Data: NewSeriesDataFromValues([]float64{ - 4200, - 3000, - 20000, - 35000, - 50000, - 18000, - }), - }, - { - Type: ChartTypeRadar, - index: 1, - Data: NewSeriesDataFromValues([]float64{ - 5000, - 14000, - 28000, - 26000, - 42000, - 21000, - }), - }, - }, - }, - }, - }) - assert.Nil(err) - data, err := d.Bytes() - assert.Nil(err) - assert.Equal("\\n2012201320142015201620170153045607590Milk TeaMatcha LatteCheese CocoaWalnut BrownieMilk Tea: 34.03%Matcha Latte: 27.66%Cheese Cocoa: 22.32%Walnut Brownie: 15.96%SalesAdministrationInformation TechnologyCustomer SupportDevelopmentMarketingAllocated BudgetActual Spending", string(data)) -} - -func BenchmarkMultiChartPNGRender(b *testing.B) { - for i := 0; i < b.N; i++ { - opt := ChartOption{ - Type: ChartOutputPNG, - Legend: LegendOption{ - Top: "-90", - Data: []string{ - "Milk Tea", - "Matcha Latte", - "Cheese Cocoa", - "Walnut Brownie", - }, - }, - Padding: chart.Box{ - Top: 100, - Right: 10, - Bottom: 10, - Left: 10, - }, - XAxis: NewXAxisOption([]string{ - "2012", - "2013", - "2014", - "2015", - "2016", - "2017", - }), - YAxisList: []YAxisOption{ - { - - Min: NewFloatPoint(0), - Max: NewFloatPoint(90), - }, - }, - SeriesList: []Series{ - NewSeriesFromValues([]float64{ - 56.5, - 82.1, - 88.7, - 70.1, - 53.4, - 85.1, - }), - NewSeriesFromValues([]float64{ - 51.1, - 51.4, - 55.1, - 53.3, - 73.8, - 68.7, - }), - NewSeriesFromValues([]float64{ - 40.1, - 62.2, - 69.5, - 36.4, - 45.2, - 32.5, - }, ChartTypeBar), - NewSeriesFromValues([]float64{ - 25.2, - 37.1, - 41.2, - 18, - 33.9, - 49.1, - }, ChartTypeBar), - }, - Children: []ChartOption{ - { - Legend: LegendOption{ - Show: FalseFlag(), - Data: []string{ - "Milk Tea", - "Matcha Latte", - "Cheese Cocoa", - "Walnut Brownie", - }, - }, - Box: chart.Box{ - Top: 20, - Left: 400, - Right: 500, - Bottom: 120, - }, - SeriesList: NewPieSeriesList([]float64{ - 435.9, - 354.3, - 285.9, - 204.5, - }, PieSeriesOption{ - Label: SeriesLabel{ - Show: true, - }, - Radius: "35%", - }), - }, - }, - } - d, err := Render(opt) - if err != nil { - panic(err) - } - buf, err := d.Bytes() - if err != nil { - panic(err) - } - if len(buf) == 0 { - panic(errors.New("data is nil")) - } - } -} - -func BenchmarkMultiChartSVGRender(b *testing.B) { - for i := 0; i < b.N; i++ { - opt := ChartOption{ - Legend: LegendOption{ - Top: "-90", - Data: []string{ - "Milk Tea", - "Matcha Latte", - "Cheese Cocoa", - "Walnut Brownie", - }, - }, - Padding: chart.Box{ - Top: 100, - Right: 10, - Bottom: 10, - Left: 10, - }, - XAxis: NewXAxisOption([]string{ - "2012", - "2013", - "2014", - "2015", - "2016", - "2017", - }), - YAxisList: []YAxisOption{ - { - - Min: NewFloatPoint(0), - Max: NewFloatPoint(90), - }, - }, - SeriesList: []Series{ - NewSeriesFromValues([]float64{ - 56.5, - 82.1, - 88.7, - 70.1, - 53.4, - 85.1, - }), - NewSeriesFromValues([]float64{ - 51.1, - 51.4, - 55.1, - 53.3, - 73.8, - 68.7, - }), - NewSeriesFromValues([]float64{ - 40.1, - 62.2, - 69.5, - 36.4, - 45.2, - 32.5, - }, ChartTypeBar), - NewSeriesFromValues([]float64{ - 25.2, - 37.1, - 41.2, - 18, - 33.9, - 49.1, - }, ChartTypeBar), - }, - Children: []ChartOption{ - { - Legend: LegendOption{ - Show: FalseFlag(), - Data: []string{ - "Milk Tea", - "Matcha Latte", - "Cheese Cocoa", - "Walnut Brownie", - }, - }, - Box: chart.Box{ - Top: 20, - Left: 400, - Right: 500, - Bottom: 120, - }, - SeriesList: NewPieSeriesList([]float64{ - 435.9, - 354.3, - 285.9, - 204.5, - }, PieSeriesOption{ - Label: SeriesLabel{ - Show: true, - }, - Radius: "35%", - }), - }, - }, - } - d, err := Render(opt) - if err != nil { - panic(err) - } - buf, err := d.Bytes() - if err != nil { - panic(err) - } - if len(buf) == 0 { - panic(errors.New("data is nil")) - } - } -} diff --git a/draw.go b/draw.go deleted file mode 100644 index 1708662..0000000 --- a/draw.go +++ /dev/null @@ -1,372 +0,0 @@ -// 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" - "github.com/wcharczuk/go-chart/v2/drawing" -) - -const ( - PositionLeft = "left" - PositionRight = "right" - PositionCenter = "center" - PositionTop = "top" - PositionBottom = "bottom" -) - -const ( - OrientHorizontal = "horizontal" - OrientVertical = "vertical" -) - -type Draw struct { - // Render - Render chart.Renderer - // The canvas box - Box chart.Box - // The font for draw - Font *truetype.Font - // The parent of draw - parent *Draw -} - -type DrawOption struct { - // Draw type, "svg" or "png", default type is "svg" - Type string - // Parent of draw - Parent *Draw - // The width of draw canvas - Width int - // The height of draw canvas - Height int -} - -type Option func(*Draw) error - -// PaddingOption sets the padding of draw canvas -func PaddingOption(padding chart.Box) Option { - return func(d *Draw) error { - d.Box.Left += padding.Left - d.Box.Top += padding.Top - d.Box.Right -= padding.Right - d.Box.Bottom -= padding.Bottom - return nil - } -} - -// BoxOption set the box of draw canvas -func BoxOption(box chart.Box) Option { - return func(d *Draw) error { - if box.IsZero() { - return nil - } - d.Box = box - return nil - } -} - -// NewDraw returns a new draw canvas -func NewDraw(opt DrawOption, opts ...Option) (*Draw, error) { - if opt.Parent == nil && (opt.Width <= 0 || opt.Height <= 0) { - return nil, errors.New("parent and width/height can not be nil") - } - font, _ := chart.GetDefaultFont() - d := &Draw{ - Font: font, - } - width := opt.Width - height := opt.Height - if opt.Parent != nil { - d.parent = opt.Parent - d.Render = d.parent.Render - d.Box = opt.Parent.Box.Clone() - } - if width != 0 && height != 0 { - d.Box.Right = width + d.Box.Left - d.Box.Bottom = height + d.Box.Top - } - // 创建render - if d.parent == nil { - fn := chart.SVG - if opt.Type == ChartOutputPNG { - fn = chart.PNG - } - r, err := fn(d.Box.Right, d.Box.Bottom) - if err != nil { - return nil, err - } - d.Render = r - } - - for _, o := range opts { - err := o(d) - if err != nil { - return nil, err - } - } - return d, nil -} - -// Parent returns the parent of draw -func (d *Draw) Parent() *Draw { - return d.parent -} - -// Top returns the top parent of draw -func (d *Draw) Top() *Draw { - if d.parent == nil { - return nil - } - t := d.parent - // 限制最多查询次数,避免嵌套引用 - for i := 50; i > 0; i-- { - if t.parent == nil { - break - } - t = t.parent - } - return t -} - -// Bytes returns the data of draw canvas -func (d *Draw) Bytes() ([]byte, error) { - buffer := bytes.Buffer{} - err := d.Render.Save(&buffer) - if err != nil { - return nil, err - } - return buffer.Bytes(), err -} - -func (d *Draw) moveTo(x, y int) { - d.Render.MoveTo(x+d.Box.Left, y+d.Box.Top) -} - -func (d *Draw) arcTo(cx, cy int, rx, ry, startAngle, delta float64) { - d.Render.ArcTo(cx+d.Box.Left, cy+d.Box.Top, rx, ry, startAngle, delta) -} - -func (d *Draw) lineTo(x, y int) { - d.Render.LineTo(x+d.Box.Left, y+d.Box.Top) -} - -func (d *Draw) pin(x, y, width int) { - r := float64(width) / 2 - y -= width / 4 - angle := chart.DegreesToRadians(15) - - startAngle := math.Pi/2 + angle - delta := 2*math.Pi - 2*angle - d.arcTo(x, y, r, r, startAngle, delta) - d.lineTo(x, y) - d.Render.Close() - d.Render.FillStroke() - - startX := x - int(r) - startY := y - endX := x + int(r) - endY := y - d.moveTo(startX, startY) - - left := d.Box.Left - top := d.Box.Top - cx := x - cy := y + int(r*2.5) - d.Render.QuadCurveTo(cx+left, cy+top, endX+left, endY+top) - d.Render.Close() - d.Render.Fill() -} - -func (d *Draw) arrowLeft(x, y, width, height int) { - d.arrow(x, y, width, height, PositionLeft) -} - -func (d *Draw) arrowRight(x, y, width, height int) { - d.arrow(x, y, width, height, PositionRight) -} - -func (d *Draw) arrowTop(x, y, width, height int) { - d.arrow(x, y, width, height, PositionTop) -} -func (d *Draw) arrowBottom(x, y, width, height int) { - d.arrow(x, y, width, height, PositionBottom) -} - -func (d *Draw) 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 - } - d.moveTo(x0, y0) - d.lineTo(x0+halfWidth, y1) - d.lineTo(x1, y0) - d.lineTo(x0+halfWidth, y+dy) - d.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 - } - d.moveTo(x0, y0) - d.lineTo(x1, y0+halfHeight) - d.lineTo(x0, y0+height) - d.lineTo(x0+dx, y0+halfHeight) - d.lineTo(x0, y0) - } - d.Render.FillStroke() -} - -func (d *Draw) makeLine(x, y, width int) { - arrowWidth := 16 - arrowHeight := 10 - endX := x + width - d.circle(3, x, y) - d.Render.Fill() - d.moveTo(x+5, y) - d.lineTo(endX-arrowWidth, y) - d.Render.Stroke() - d.Render.SetStrokeDashArray([]float64{}) - d.arrowRight(endX, y, arrowWidth, arrowHeight) -} - -func (d *Draw) circle(radius float64, x, y int) { - d.Render.Circle(radius, x+d.Box.Left, y+d.Box.Top) -} - -func (d *Draw) text(body string, x, y int) { - d.Render.Text(body, x+d.Box.Left, y+d.Box.Top) -} - -func (d *Draw) textFit(body string, x, y, width int, style chart.Style) chart.Box { - style.TextWrap = chart.TextWrapWord - r := d.Render - lines := chart.Text.WrapFit(r, body, width, style) - style.WriteTextOptionsToRenderer(r) - var output chart.Box - - for index, line := range lines { - x0 := x - y0 := y + output.Height() - d.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() - } - } - return output -} - -func (d *Draw) measureTextFit(body string, x, y, width int, style chart.Style) chart.Box { - style.TextWrap = chart.TextWrapWord - r := d.Render - lines := chart.Text.WrapFit(r, body, width, style) - style.WriteTextOptionsToRenderer(r) - return chart.Text.MeasureLines(r, lines, style) -} - -func (d *Draw) lineStroke(points []Point, style LineStyle) { - s := style.Style() - if !s.ShouldDrawStroke() { - return - } - r := d.Render - s.GetStrokeOptions().WriteDrawingOptionsToRenderer(r) - for index, point := range points { - x := point.X - y := point.Y - if index == 0 { - d.moveTo(x, y) - } else { - d.lineTo(x, y) - } - } - r.Stroke() -} - -func (d *Draw) setBackground(width, height int, color drawing.Color) { - r := d.Render - s := chart.Style{ - FillColor: color, - } - s.WriteToRenderer(r) - r.MoveTo(0, 0) - r.LineTo(width, 0) - r.LineTo(width, height) - r.LineTo(0, height) - r.LineTo(0, 0) - r.FillStroke() -} - -func (d *Draw) polygon(center Point, radius float64, sides int) { - points := getPolygonPoints(center, radius, sides) - for i, p := range points { - if i == 0 { - d.moveTo(p.X, p.Y) - } else { - d.lineTo(p.X, p.Y) - } - } - d.lineTo(points[0].X, points[0].Y) - d.Render.Stroke() -} - -func (d *Draw) fill(points []Point, s chart.Style) { - if !s.ShouldDrawFill() { - return - } - r := d.Render - var x, y int - s.GetFillOptions().WriteDrawingOptionsToRenderer(r) - for index, point := range points { - x = point.X - y = point.Y - if index == 0 { - d.moveTo(x, y) - } else { - d.lineTo(x, y) - } - } - r.Fill() -} diff --git a/draw_test.go b/draw_test.go deleted file mode 100644 index f6a3dd1..0000000 --- a/draw_test.go +++ /dev/null @@ -1,507 +0,0 @@ -// 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/stretchr/testify/assert" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" -) - -func TestParentOption(t *testing.T) { - assert := assert.New(t) - p, err := NewDraw(DrawOption{ - Width: 400, - Height: 300, - }) - assert.Nil(err) - - d, err := NewDraw(DrawOption{ - Parent: p, - }) - assert.Nil(err) - assert.Equal(p, d.parent) -} - -func TestWidthHeightOption(t *testing.T) { - assert := assert.New(t) - - // no parent - width := 300 - height := 200 - d, err := NewDraw(DrawOption{ - Width: width, - Height: height, - }) - assert.Nil(err) - assert.Equal(chart.Box{ - Top: 0, - Left: 0, - Right: width, - Bottom: height, - }, d.Box) - - width = 500 - height = 600 - // with parent - p, err := NewDraw( - DrawOption{ - Width: width, - Height: height, - }, - PaddingOption(chart.NewBox(5, 5, 5, 5)), - ) - assert.Nil(err) - d, err = NewDraw( - DrawOption{ - Parent: p, - }, - PaddingOption(chart.NewBox(1, 2, 3, 4)), - ) - assert.Nil(err) - assert.Equal(chart.Box{ - Top: 6, - Left: 7, - Right: 492, - Bottom: 591, - }, d.Box) -} - -func TestBoxOption(t *testing.T) { - assert := assert.New(t) - - d, err := NewDraw(DrawOption{ - Width: 400, - Height: 300, - }) - assert.Nil(err) - - err = BoxOption(chart.Box{ - Left: 10, - Top: 20, - Right: 50, - Bottom: 100, - })(d) - assert.Nil(err) - assert.Equal(chart.Box{ - Left: 10, - Top: 20, - Right: 50, - Bottom: 100, - }, d.Box) - - // zero box will be ignored - err = BoxOption(chart.Box{})(d) - assert.Nil(err) - assert.Equal(chart.Box{ - Left: 10, - Top: 20, - Right: 50, - Bottom: 100, - }, d.Box) -} - -func TestPaddingOption(t *testing.T) { - assert := assert.New(t) - - d, err := NewDraw(DrawOption{ - Width: 400, - Height: 300, - }) - assert.Nil(err) - - // 默认的box - assert.Equal(chart.Box{ - Right: 400, - Bottom: 300, - }, d.Box) - - // 设置padding之后的box - d, err = NewDraw(DrawOption{ - Width: 400, - Height: 300, - }, PaddingOption(chart.Box{ - Left: 1, - Top: 2, - Right: 3, - Bottom: 4, - })) - assert.Nil(err) - assert.Equal(chart.Box{ - Top: 2, - Left: 1, - Right: 397, - Bottom: 296, - }, d.Box) - - p := d - // 设置父元素之后的box - d, err = NewDraw( - DrawOption{ - Parent: p, - }, - PaddingOption(chart.Box{ - Left: 1, - Top: 2, - Right: 3, - Bottom: 4, - }), - ) - assert.Nil(err) - assert.Equal(chart.Box{ - Top: 4, - Left: 2, - Right: 394, - Bottom: 292, - }, d.Box) -} - -func TestParentTop(t *testing.T) { - assert := assert.New(t) - d1, err := NewDraw(DrawOption{ - Width: 400, - Height: 300, - }) - assert.Nil(err) - - d2, err := NewDraw(DrawOption{ - Parent: d1, - }) - assert.Nil(err) - - d3, err := NewDraw(DrawOption{ - Parent: d2, - }) - assert.Nil(err) - - assert.Equal(d2, d3.Parent()) - assert.Equal(d1, d2.Parent()) - assert.Equal(d1, d3.Top()) - assert.Equal(d1, d2.Top()) -} - -func TestDraw(t *testing.T) { - assert := assert.New(t) - - tests := []struct { - fn func(d *Draw) - result string - }{ - // moveTo, lineTo - { - fn: func(d *Draw) { - d.moveTo(1, 1) - d.lineTo(2, 2) - d.Render.Stroke() - }, - result: "\\n", - }, - // circle - { - fn: func(d *Draw) { - d.circle(5, 2, 3) - }, - result: "\\n", - }, - // text - { - fn: func(d *Draw) { - d.text("hello world!", 3, 6) - }, - result: "\\nhello world!", - }, - // line stroke - { - fn: func(d *Draw) { - d.lineStroke([]Point{ - { - X: 1, - Y: 2, - }, - { - X: 3, - Y: 4, - }, - }, LineStyle{ - StrokeColor: drawing.ColorBlack, - StrokeWidth: 1, - }) - }, - result: "\\n", - }, - // set background - { - fn: func(d *Draw) { - d.setBackground(400, 300, chart.ColorWhite) - }, - result: "\\n", - }, - // arcTo - { - fn: func(d *Draw) { - chart.Style{ - StrokeWidth: 1, - StrokeColor: drawing.ColorBlack, - FillColor: drawing.ColorBlue, - }.WriteToRenderer(d.Render) - d.arcTo(100, 100, 100, 100, 0, math.Pi/2) - d.Render.Close() - d.Render.FillStroke() - }, - result: "\\n", - }, - // pin - { - fn: func(d *Draw) { - chart.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, - }, - }.WriteToRenderer(d.Render) - d.pin(30, 30, 30) - }, - result: "\\n", - }, - // arrow left - { - fn: func(d *Draw) { - chart.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, - }, - }.WriteToRenderer(d.Render) - d.arrowLeft(30, 30, 16, 10) - }, - result: "\\n", - }, - // arrow right - { - fn: func(d *Draw) { - chart.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, - }, - }.WriteToRenderer(d.Render) - d.arrowRight(30, 30, 16, 10) - }, - result: "\\n", - }, - // arrow top - { - fn: func(d *Draw) { - chart.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, - }, - }.WriteToRenderer(d.Render) - d.arrowTop(30, 30, 10, 16) - }, - result: "\\n", - }, - // arrow bottom - { - fn: func(d *Draw) { - chart.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, - }, - }.WriteToRenderer(d.Render) - d.arrowBottom(30, 30, 10, 16) - }, - result: "\\n", - }, - // mark line - { - fn: func(d *Draw) { - chart.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, - }, - }.WriteToRenderer(d.Render) - d.makeLine(0, 20, 300) - }, - result: "\\n", - }, - // polygon - { - fn: func(d *Draw) { - chart.Style{ - StrokeWidth: 1, - StrokeColor: drawing.Color{ - R: 84, - G: 112, - B: 198, - A: 255, - }, - }.WriteToRenderer(d.Render) - d.polygon(Point{ - X: 100, - Y: 100, - }, 50, 6) - }, - result: "\\n", - }, - // fill - { - fn: func(d *Draw) { - d.fill([]Point{ - { - X: 0, - Y: 0, - }, - { - X: 0, - Y: 100, - }, - { - X: 100, - Y: 100, - }, - { - X: 0, - Y: 0, - }, - }, chart.Style{ - FillColor: drawing.Color{ - R: 84, - G: 112, - B: 198, - A: 255, - }, - }) - }, - result: "\\n", - }, - } - for _, tt := range tests { - d, err := NewDraw(DrawOption{ - Width: 400, - Height: 300, - }, PaddingOption(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 TestDrawTextFit(t *testing.T) { - assert := assert.New(t) - d, err := NewDraw(DrawOption{ - Width: 400, - Height: 300, - }) - assert.Nil(err) - f, _ := chart.GetDefaultFont() - style := chart.Style{ - FontSize: 12, - FontColor: chart.ColorBlack, - Font: f, - } - box := d.textFit("Hello World!", 0, 20, 80, style) - assert.Equal(chart.Box{ - Right: 45, - Bottom: 35, - }, box) - - box = d.textFit("Hello World!", 0, 100, 200, style) - assert.Equal(chart.Box{ - Right: 84, - Bottom: 15, - }, box) - - buf, err := d.Bytes() - assert.Nil(err) - assert.Equal(`\nHelloWorld!Hello World!`, string(buf)) -} diff --git a/echarts.go b/echarts.go deleted file mode 100644 index 4ebb9ad..0000000 --- a/echarts.go +++ /dev/null @@ -1,499 +0,0 @@ -// 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" - "encoding/json" - "fmt" - "regexp" - "strconv" - - "github.com/wcharczuk/go-chart/v2" -) - -func convertToArray(data []byte) []byte { - data = bytes.TrimSpace(data) - if len(data) == 0 { - return nil - } - if data[0] != '[' { - data = []byte("[" + string(data) + "]") - } - return data -} - -type EChartsPosition string - -func (p *EChartsPosition) UnmarshalJSON(data []byte) error { - if len(data) == 0 { - return nil - } - if regexp.MustCompile(`^\d+`).Match(data) { - data = []byte(fmt.Sprintf(`"%s"`, string(data))) - } - s := (*string)(p) - return json.Unmarshal(data, s) -} - -type EChartStyle struct { - Color string `json:"color"` -} - -func (es *EChartStyle) ToStyle() chart.Style { - color := parseColor(es.Color) - return chart.Style{ - FillColor: color, - FontColor: color, - StrokeColor: color, - } -} - -type EChartsSeriesDataValue struct { - values []float64 -} - -func (value *EChartsSeriesDataValue) UnmarshalJSON(data []byte) error { - data = convertToArray(data) - return json.Unmarshal(data, &value.values) -} -func (value *EChartsSeriesDataValue) First() float64 { - if len(value.values) == 0 { - return 0 - } - return value.values[0] -} -func NewEChartsSeriesDataValue(values ...float64) EChartsSeriesDataValue { - return EChartsSeriesDataValue{ - values: values, - } -} - -type EChartsSeriesData struct { - Value EChartsSeriesDataValue `json:"value"` - Name string `json:"name"` - ItemStyle EChartStyle `json:"itemStyle"` -} -type _EChartsSeriesData EChartsSeriesData - -var numericRep = regexp.MustCompile(`^[-+]?[0-9]+(?:\.[0-9]+)?$`) - -func (es *EChartsSeriesData) UnmarshalJSON(data []byte) error { - data = bytes.TrimSpace(data) - if len(data) == 0 { - return nil - } - if numericRep.Match(data) { - v, err := strconv.ParseFloat(string(data), 64) - if err != nil { - return err - } - es.Value = EChartsSeriesDataValue{ - values: []float64{ - v, - }, - } - return nil - } - v := _EChartsSeriesData{} - err := json.Unmarshal(data, &v) - if err != nil { - return err - } - es.Name = v.Name - es.Value = v.Value - es.ItemStyle = v.ItemStyle - return nil -} - -type EChartsXAxisData struct { - BoundaryGap *bool `json:"boundaryGap"` - SplitNumber int `json:"splitNumber"` - Data []string `json:"data"` -} -type EChartsXAxis struct { - Data []EChartsXAxisData -} - -func (ex *EChartsXAxis) UnmarshalJSON(data []byte) error { - data = convertToArray(data) - if len(data) == 0 { - return nil - } - return json.Unmarshal(data, &ex.Data) -} - -type EChartsAxisLabel struct { - Formatter string `json:"formatter"` -} -type EChartsYAxisData struct { - Min *float64 `json:"min"` - Max *float64 `json:"max"` - AxisLabel EChartsAxisLabel `json:"axisLabel"` - AxisLine struct { - LineStyle struct { - Color string `json:"color"` - } `json:"lineStyle"` - } `json:"axisLine"` -} -type EChartsYAxis struct { - Data []EChartsYAxisData `json:"data"` -} - -func (ey *EChartsYAxis) UnmarshalJSON(data []byte) error { - data = convertToArray(data) - if len(data) == 0 { - return nil - } - return json.Unmarshal(data, &ey.Data) -} - -type EChartsPadding struct { - Box chart.Box -} - -func (eb *EChartsPadding) UnmarshalJSON(data []byte) error { - data = convertToArray(data) - if len(data) == 0 { - return nil - } - arr := make([]int, 0) - err := json.Unmarshal(data, &arr) - if err != nil { - return err - } - if len(arr) == 0 { - return nil - } - switch len(arr) { - case 1: - eb.Box = chart.Box{ - Left: arr[0], - Top: arr[0], - Bottom: arr[0], - Right: arr[0], - } - case 2: - eb.Box = chart.Box{ - Top: arr[0], - Bottom: arr[0], - Left: arr[1], - Right: arr[1], - } - default: - result := make([]int, 4) - copy(result, arr) - if len(arr) == 3 { - result[3] = result[1] - } - // 上右下左 - eb.Box = chart.Box{ - Top: result[0], - Right: result[1], - Bottom: result[2], - Left: result[3], - } - } - return nil -} - -type EChartsLabelOption struct { - Show bool `json:"show"` - Distance int `json:"distance"` - Color string `json:"color"` -} -type EChartsLegend struct { - Show *bool `json:"show"` - Data []string `json:"data"` - Align string `json:"align"` - Orient string `json:"orient"` - Padding EChartsPadding `json:"padding"` - Left EChartsPosition `json:"left"` - Top EChartsPosition `json:"top"` - TextStyle EChartsTextStyle `json:"textStyle"` -} - -type EChartsMarkData struct { - Type string `json:"type"` -} -type _EChartsMarkData EChartsMarkData - -func (emd *EChartsMarkData) UnmarshalJSON(data []byte) error { - data = bytes.TrimSpace(data) - if len(data) == 0 { - return nil - } - data = convertToArray(data) - ds := make([]*_EChartsMarkData, 0) - err := json.Unmarshal(data, &ds) - if err != nil { - return err - } - for _, d := range ds { - if d.Type != "" { - emd.Type = d.Type - } - } - return nil -} - -type EChartsMarkPoint struct { - SymbolSize int `json:"symbolSize"` - Data []EChartsMarkData `json:"data"` -} - -func (emp *EChartsMarkPoint) ToSeriesMarkPoint() SeriesMarkPoint { - sp := SeriesMarkPoint{ - SymbolSize: emp.SymbolSize, - } - if len(emp.Data) == 0 { - return sp - } - data := make([]SeriesMarkData, len(emp.Data)) - for index, item := range emp.Data { - data[index].Type = item.Type - } - sp.Data = data - return sp -} - -type EChartsMarkLine struct { - Data []EChartsMarkData `json:"data"` -} - -func (eml *EChartsMarkLine) ToSeriesMarkLine() SeriesMarkLine { - sl := SeriesMarkLine{} - if len(eml.Data) == 0 { - return sl - } - data := make([]SeriesMarkData, len(eml.Data)) - for index, item := range eml.Data { - data[index].Type = item.Type - } - sl.Data = data - return sl -} - -type EChartsSeries struct { - Data []EChartsSeriesData `json:"data"` - Name string `json:"name"` - Type string `json:"type"` - Radius string `json:"radius"` - YAxisIndex int `json:"yAxisIndex"` - ItemStyle EChartStyle `json:"itemStyle"` - // label的配置 - Label EChartsLabelOption `json:"label"` - MarkPoint EChartsMarkPoint `json:"markPoint"` - MarkLine EChartsMarkLine `json:"markLine"` - Max *float64 `json:"max"` - Min *float64 `json:"min"` -} -type EChartsSeriesList []EChartsSeries - -func (esList EChartsSeriesList) ToSeriesList() SeriesList { - seriesList := make(SeriesList, 0, len(esList)) - for _, item := range esList { - // 如果是pie,则每个子荐生成一个series - if item.Type == ChartTypePie { - for _, dataItem := range item.Data { - seriesList = append(seriesList, Series{ - Type: item.Type, - Name: dataItem.Name, - Label: SeriesLabel{ - Show: true, - }, - Radius: item.Radius, - Data: []SeriesData{ - { - Value: dataItem.Value.First(), - }, - }, - }) - } - continue - } - // 如果是radar或funnel - if item.Type == ChartTypeRadar || - item.Type == ChartTypeFunnel { - for _, dataItem := range item.Data { - seriesList = append(seriesList, Series{ - Name: dataItem.Name, - Type: item.Type, - Data: NewSeriesDataFromValues(dataItem.Value.values), - Max: item.Max, - Min: item.Min, - }) - } - continue - } - data := make([]SeriesData, len(item.Data)) - for j, dataItem := range item.Data { - data[j] = SeriesData{ - Value: dataItem.Value.First(), - Style: dataItem.ItemStyle.ToStyle(), - } - } - seriesList = append(seriesList, Series{ - Type: item.Type, - Data: data, - YAxisIndex: item.YAxisIndex, - Style: item.ItemStyle.ToStyle(), - Label: SeriesLabel{ - Color: parseColor(item.Label.Color), - Show: item.Label.Show, - Distance: item.Label.Distance, - }, - Name: item.Name, - MarkPoint: item.MarkPoint.ToSeriesMarkPoint(), - MarkLine: item.MarkLine.ToSeriesMarkLine(), - }) - } - return seriesList -} - -type EChartsTextStyle struct { - Color string `json:"color"` - FontFamily string `json:"fontFamily"` - FontSize float64 `json:"fontSize"` -} - -func (et *EChartsTextStyle) ToStyle() chart.Style { - s := chart.Style{ - FontSize: et.FontSize, - FontColor: parseColor(et.Color), - } - if et.FontFamily != "" { - s.Font, _ = GetFont(et.FontFamily) - } - return s -} - -type EChartsOption struct { - Type string `json:"type"` - Theme string `json:"theme"` - FontFamily string `json:"fontFamily"` - Padding EChartsPadding `json:"padding"` - Box chart.Box `json:"box"` - Width int `json:"width"` - Height int `json:"height"` - Title struct { - Text string `json:"text"` - Subtext string `json:"subtext"` - Left EChartsPosition `json:"left"` - Top EChartsPosition `json:"top"` - TextStyle EChartsTextStyle `json:"textStyle"` - SubtextStyle EChartsTextStyle `json:"subtextStyle"` - } `json:"title"` - XAxis EChartsXAxis `json:"xAxis"` - YAxis EChartsYAxis `json:"yAxis"` - Legend EChartsLegend `json:"legend"` - Radar struct { - Indicator []RadarIndicator `json:"indicator"` - } `json:"radar"` - Series EChartsSeriesList `json:"series"` - Children []EChartsOption `json:"children"` -} - -func (eo *EChartsOption) ToOption() ChartOption { - fontFamily := eo.FontFamily - if len(fontFamily) == 0 { - fontFamily = eo.Title.TextStyle.FontFamily - } - o := ChartOption{ - Type: eo.Type, - FontFamily: fontFamily, - Theme: eo.Theme, - Title: TitleOption{ - Text: eo.Title.Text, - Subtext: eo.Title.Subtext, - Style: eo.Title.TextStyle.ToStyle(), - SubtextStyle: eo.Title.SubtextStyle.ToStyle(), - Left: string(eo.Title.Left), - Top: string(eo.Title.Top), - }, - Legend: LegendOption{ - Show: eo.Legend.Show, - Style: eo.Legend.TextStyle.ToStyle(), - Data: eo.Legend.Data, - Left: string(eo.Legend.Left), - Top: string(eo.Legend.Top), - Align: eo.Legend.Align, - Orient: eo.Legend.Orient, - }, - RadarIndicators: eo.Radar.Indicator, - Width: eo.Width, - Height: eo.Height, - Padding: eo.Padding.Box, - Box: eo.Box, - SeriesList: eo.Series.ToSeriesList(), - } - if len(eo.XAxis.Data) != 0 { - xAxisData := eo.XAxis.Data[0] - o.XAxis = XAxisOption{ - BoundaryGap: xAxisData.BoundaryGap, - Data: xAxisData.Data, - SplitNumber: xAxisData.SplitNumber, - } - } - yAxisOptions := make([]YAxisOption, len(eo.YAxis.Data)) - for index, item := range eo.YAxis.Data { - yAxisOptions[index] = YAxisOption{ - Min: item.Min, - Max: item.Max, - Formatter: item.AxisLabel.Formatter, - Color: parseColor(item.AxisLine.LineStyle.Color), - } - } - o.YAxisList = yAxisOptions - - if len(eo.Children) != 0 { - o.Children = make([]ChartOption, len(eo.Children)) - for index, item := range eo.Children { - o.Children[index] = item.ToOption() - } - } - return o -} - -func renderEcharts(options, outputType string) ([]byte, error) { - o := EChartsOption{} - err := json.Unmarshal([]byte(options), &o) - if err != nil { - return nil, err - } - opt := o.ToOption() - opt.Type = outputType - d, err := Render(opt) - if err != nil { - return nil, err - } - return d.Bytes() -} - -func RenderEChartsToPNG(options string) ([]byte, error) { - return renderEcharts(options, "png") -} - -func RenderEChartsToSVG(options string) ([]byte, error) { - return renderEcharts(options, "svg") -} diff --git a/echarts_test.go b/echarts_test.go deleted file mode 100644 index 05c2a40..0000000 --- a/echarts_test.go +++ /dev/null @@ -1,592 +0,0 @@ -// 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 ( - "encoding/json" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" -) - -func TestEChartsPosition(t *testing.T) { - assert := assert.New(t) - - var p EChartsPosition - err := p.UnmarshalJSON([]byte("12")) - assert.Nil(err) - assert.Equal("12", string(p)) - - err = p.UnmarshalJSON([]byte(`"12%"`)) - assert.Nil(err) - assert.Equal("12%", string(p)) -} -func TestEChartStyle(t *testing.T) { - assert := assert.New(t) - - s := EChartStyle{ - Color: "#aaa", - } - r := drawing.Color{ - R: 170, - G: 170, - B: 170, - A: 255, - } - assert.Equal(chart.Style{ - FillColor: r, - FontColor: r, - StrokeColor: r, - }, s.ToStyle()) -} - -func TestEChartsXAxis(t *testing.T) { - assert := assert.New(t) - ex := EChartsXAxis{} - err := ex.UnmarshalJSON([]byte(`{ - "boundaryGap": false, - "splitNumber": 5, - "data": [ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun" - ] - }`)) - assert.Nil(err) - assert.Equal(EChartsXAxis{ - Data: []EChartsXAxisData{ - { - BoundaryGap: FalseFlag(), - SplitNumber: 5, - Data: []string{ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun", - }, - }, - }, - }, ex) -} - -func TestEChartsYAxis(t *testing.T) { - assert := assert.New(t) - ey := EChartsYAxis{} - - err := ey.UnmarshalJSON([]byte(`{ - "min": 1, - "max": 10, - "axisLabel": { - "formatter": "ab" - } - }`)) - assert.Nil(err) - assert.Equal(EChartsYAxis{ - Data: []EChartsYAxisData{ - { - Min: NewFloatPoint(1), - Max: NewFloatPoint(10), - AxisLabel: EChartsAxisLabel{ - Formatter: "ab", - }, - }, - }, - }, ey) - - ey = EChartsYAxis{} - err = ey.UnmarshalJSON([]byte(`[ - { - "min": 1, - "max": 10, - "axisLabel": { - "formatter": "ab" - } - }, - { - "min": 2, - "max": 20, - "axisLabel": { - "formatter": "cd" - } - } - ]`)) - assert.Nil(err) - assert.Equal(EChartsYAxis{ - Data: []EChartsYAxisData{ - { - Min: NewFloatPoint(1), - Max: NewFloatPoint(10), - AxisLabel: EChartsAxisLabel{ - Formatter: "ab", - }, - }, - { - Min: NewFloatPoint(2), - Max: NewFloatPoint(20), - AxisLabel: EChartsAxisLabel{ - Formatter: "cd", - }, - }, - }, - }, ey) -} - -func TestEChartsPadding(t *testing.T) { - assert := assert.New(t) - - ep := EChartsPadding{} - - err := ep.UnmarshalJSON([]byte(`10`)) - assert.Nil(err) - assert.Equal(EChartsPadding{ - Box: chart.Box{ - Top: 10, - Right: 10, - Bottom: 10, - Left: 10, - }, - }, ep) - - ep = EChartsPadding{} - err = ep.UnmarshalJSON([]byte(`[10, 20]`)) - assert.Nil(err) - assert.Equal(EChartsPadding{ - Box: chart.Box{ - Top: 10, - Right: 20, - Bottom: 10, - Left: 20, - }, - }, ep) - - ep = EChartsPadding{} - err = ep.UnmarshalJSON([]byte(`[10, 20, 30]`)) - assert.Nil(err) - assert.Equal(EChartsPadding{ - Box: chart.Box{ - Top: 10, - Right: 20, - Bottom: 30, - Left: 20, - }, - }, ep) - - ep = EChartsPadding{} - err = ep.UnmarshalJSON([]byte(`[10, 20, 30, 40]`)) - assert.Nil(err) - assert.Equal(EChartsPadding{ - Box: chart.Box{ - Top: 10, - Right: 20, - Bottom: 30, - Left: 40, - }, - }, ep) - -} -func TestEChartsLegend(t *testing.T) { - assert := assert.New(t) - - el := EChartsLegend{} - - err := json.Unmarshal([]byte(`{ - "data": ["a", "b", "c"], - "align": "right", - "padding": [10], - "left": "20%", - "top": 10 - }`), &el) - assert.Nil(err) - assert.Equal(EChartsLegend{ - Data: []string{ - "a", - "b", - "c", - }, - Align: "right", - Padding: EChartsPadding{ - Box: chart.Box{ - Left: 10, - Top: 10, - Right: 10, - Bottom: 10, - }, - }, - Left: EChartsPosition("20%"), - Top: EChartsPosition("10"), - }, el) -} - -func TestEChartsSeriesData(t *testing.T) { - assert := assert.New(t) - - esd := EChartsSeriesData{} - err := esd.UnmarshalJSON([]byte(`123`)) - assert.Nil(err) - assert.Equal(EChartsSeriesData{ - Value: NewEChartsSeriesDataValue(123), - }, esd) - - esd = EChartsSeriesData{} - err = esd.UnmarshalJSON([]byte(`2.1`)) - assert.Nil(err) - assert.Equal(EChartsSeriesData{ - Value: NewEChartsSeriesDataValue(2.1), - }, esd) - - esd = EChartsSeriesData{} - err = esd.UnmarshalJSON([]byte(`{ - "value": 123.12, - "name": "test", - "itemStyle": { - "color": "#aaa" - } - }`)) - assert.Nil(err) - assert.Equal(EChartsSeriesData{ - Value: NewEChartsSeriesDataValue(123.12), - Name: "test", - ItemStyle: EChartStyle{ - Color: "#aaa", - }, - }, esd) -} - -func TestEChartsSeries(t *testing.T) { - assert := assert.New(t) - - esList := make([]EChartsSeries, 0) - err := json.Unmarshal([]byte(`[ - { - "name": "Email", - "data": [ - 120, - 132 - ] - }, - { - "name": "Union Ads", - "type": "bar", - "data": [ - 220, - 182 - ] - } - ]`), &esList) - assert.Nil(err) - assert.Equal([]EChartsSeries{ - { - Name: "Email", - Data: []EChartsSeriesData{ - { - Value: NewEChartsSeriesDataValue(120), - }, - { - Value: NewEChartsSeriesDataValue(132), - }, - }, - }, - { - Name: "Union Ads", - Type: "bar", - Data: []EChartsSeriesData{ - { - Value: NewEChartsSeriesDataValue(220), - }, - { - Value: NewEChartsSeriesDataValue(182), - }, - }, - }, - }, esList) -} - -func TestEChartsMarkData(t *testing.T) { - assert := assert.New(t) - - emd := EChartsMarkData{} - err := emd.UnmarshalJSON([]byte(`{"type": "average"}`)) - assert.Nil(err) - assert.Equal("average", emd.Type) - - emd = EChartsMarkData{} - err = emd.UnmarshalJSON([]byte(`[{}, {"type": "average"}]`)) - assert.Nil(err) - assert.Equal("average", emd.Type) -} - -func TestEChartsMarkPoint(t *testing.T) { - assert := assert.New(t) - - p := EChartsMarkPoint{} - - err := json.Unmarshal([]byte(`{ - "symbolSize": 30, - "data": [ - { - "type": "max" - }, - { - "type": "min" - } - ] - }`), &p) - assert.Nil(err) - assert.Equal(SeriesMarkPoint{ - SymbolSize: 30, - Data: []SeriesMarkData{ - { - Type: "max", - }, - { - Type: "min", - }, - }, - }, p.ToSeriesMarkPoint()) -} - -func TestEChartsMarkLine(t *testing.T) { - assert := assert.New(t) - l := EChartsMarkLine{} - - err := json.Unmarshal([]byte(`{ - "data": [ - { - "type": "max" - }, - { - "type": "min" - } - ] - }`), &l) - assert.Nil(err) - assert.Equal(SeriesMarkLine{ - Data: []SeriesMarkData{ - { - Type: "max", - }, - { - Type: "min", - }, - }, - }, l.ToSeriesMarkLine()) -} - -func TestEChartsTextStyle(t *testing.T) { - assert := assert.New(t) - - s := EChartsTextStyle{ - Color: "#aaa", - FontFamily: "test", - FontSize: 14, - } - assert.Equal(chart.Style{ - FontColor: drawing.Color{ - R: 170, - G: 170, - B: 170, - A: 255, - }, - FontSize: 14, - }, s.ToStyle()) -} - -func TestEChartsSeriesList(t *testing.T) { - assert := assert.New(t) - - // pie - es := EChartsSeriesList{ - { - Type: ChartTypePie, - Radius: "30%", - Data: []EChartsSeriesData{ - { - Name: "1", - Value: EChartsSeriesDataValue{ - values: []float64{ - 1, - }, - }, - }, - { - Name: "2", - Value: EChartsSeriesDataValue{ - values: []float64{ - 2, - }, - }, - }, - }, - }, - } - assert.Equal(SeriesList{ - { - Type: ChartTypePie, - Name: "1", - Label: SeriesLabel{ - Show: true, - }, - Radius: "30%", - Data: []SeriesData{ - { - Value: 1, - }, - }, - }, - { - Type: ChartTypePie, - Name: "2", - Label: SeriesLabel{ - Show: true, - }, - Radius: "30%", - Data: []SeriesData{ - { - Value: 2, - }, - }, - }, - }, es.ToSeriesList()) - - es = EChartsSeriesList{ - { - Type: ChartTypeBar, - Data: []EChartsSeriesData{ - { - Value: NewEChartsSeriesDataValue(1), - ItemStyle: EChartStyle{ - Color: "#aaa", - }, - }, - { - Value: NewEChartsSeriesDataValue(2), - }, - }, - YAxisIndex: 1, - }, - { - Data: []EChartsSeriesData{ - { - Value: NewEChartsSeriesDataValue(3), - }, - { - Value: NewEChartsSeriesDataValue(4), - }, - }, - ItemStyle: EChartStyle{ - Color: "#ccc", - }, - Label: EChartsLabelOption{ - Color: "#ddd", - Show: true, - Distance: 5, - }, - }, - } - assert.Equal(SeriesList{ - { - Type: ChartTypeBar, - Data: []SeriesData{ - { - Value: 1, - Style: chart.Style{ - FontColor: drawing.Color{ - R: 170, - G: 170, - B: 170, - A: 255, - }, - StrokeColor: drawing.Color{ - R: 170, - G: 170, - B: 170, - A: 255, - }, - FillColor: drawing.Color{ - R: 170, - G: 170, - B: 170, - A: 255, - }, - }, - }, - { - Value: 2, - }, - }, - YAxisIndex: 1, - }, - { - Data: []SeriesData{ - { - Value: 3, - }, - { - Value: 4, - }, - }, - Style: chart.Style{ - FontColor: drawing.Color{ - R: 204, - G: 204, - B: 204, - A: 255, - }, - StrokeColor: drawing.Color{ - R: 204, - G: 204, - B: 204, - A: 255, - }, - FillColor: drawing.Color{ - R: 204, - G: 204, - B: 204, - A: 255, - }, - }, - Label: SeriesLabel{ - Color: drawing.Color{ - R: 221, - G: 221, - B: 221, - A: 255, - }, - Show: true, - Distance: 5, - }, - MarkPoint: SeriesMarkPoint{}, - MarkLine: SeriesMarkLine{}, - }, - }, es.ToSeriesList()) - -} diff --git a/examples/basic/main.go b/examples/basic/main.go deleted file mode 100644 index 1e7af8d..0000000 --- a/examples/basic/main.go +++ /dev/null @@ -1,94 +0,0 @@ -package main - -import ( - "io/ioutil" - "os" - "path/filepath" - - charts "github.com/vicanso/go-charts" -) - -func writeFile(file string, buf []byte) error { - tmpPath := "./tmp" - err := os.MkdirAll(tmpPath, 0700) - if err != nil { - return err - } - - file = filepath.Join(tmpPath, file) - err = ioutil.WriteFile(file, buf, 0600) - if err != nil { - return err - } - return nil -} - -func chartsRender() ([]byte, error) { - d, err := charts.LineRender([][]float64{ - { - 150, - 230, - 224, - 218, - 135, - 147, - 260, - }, - }, - // output type - charts.PNGTypeOption(), - // title - charts.TitleOptionFunc(charts.TitleOption{ - Text: "Line", - }), - // x axis - charts.XAxisOptionFunc(charts.NewXAxisOption([]string{ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun", - })), - ) - if err != nil { - return nil, err - } - return d.Bytes() -} - -func echartsRender() ([]byte, error) { - return charts.RenderEChartsToPNG(`{ - "title": { - "text": "Line" - }, - "xAxis": { - "data": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] - }, - "series": [ - { - "data": [150, 230, 224, 218, 135, 147, 260] - } - ] - }`) -} - -type Render func() ([]byte, error) - -func main() { - m := map[string]Render{ - "charts-line.png": chartsRender, - "echarts-line.png": echartsRender, - } - for name, fn := range m { - buf, err := fn() - if err != nil { - panic(err) - } - err = writeFile(name, buf) - if err != nil { - panic(err) - } - } -} diff --git a/examples/charts/main.go b/examples/charts/main.go deleted file mode 100644 index fddbe6d..0000000 --- a/examples/charts/main.go +++ /dev/null @@ -1,1809 +0,0 @@ -package main - -import ( - "bytes" - "net/http" - "strconv" - - charts "github.com/vicanso/go-charts" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" -) - -var html = ` - - - - - - - go-charts - - -
{{body}}
- - -` - -func handler(w http.ResponseWriter, req *http.Request, chartOptions []charts.ChartOption, echartsOptions []string) { - if req.URL.Path != "/" && - req.URL.Path != "/echarts" { - return - } - query := req.URL.Query() - theme := query.Get("theme") - width, _ := strconv.Atoi(query.Get("width")) - height, _ := strconv.Atoi(query.Get("height")) - charts.SetDefaultWidth(width) - charts.SetDefaultWidth(height) - bytesList := make([][]byte, 0) - for _, opt := range chartOptions { - opt.Theme = theme - d, err := charts.Render(opt) - if err != nil { - panic(err) - } - buf, err := d.Bytes() - if err != nil { - panic(err) - } - bytesList = append(bytesList, buf) - } - for _, opt := range echartsOptions { - buf, err := charts.RenderEChartsToSVG(opt) - if err != nil { - panic(err) - } - bytesList = append(bytesList, buf) - } - - data := bytes.ReplaceAll([]byte(html), []byte("{{body}}"), bytes.Join(bytesList, []byte(""))) - w.Header().Set("Content-Type", "text/html") - w.Write(data) -} - -func indexHandler(w http.ResponseWriter, req *http.Request) { - chartOptions := []charts.ChartOption{ - // 普通折线图 - { - Title: charts.TitleOption{ - Text: "Line", - }, - Legend: charts.NewLegendOption([]string{ - "Email", - "Union Ads", - "Video Ads", - "Direct", - "Search Engine", - }), - XAxis: charts.NewXAxisOption([]string{ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun", - }), - SeriesList: []charts.Series{ - charts.NewSeriesFromValues([]float64{ - 120, - 132, - 101, - 134, - 90, - 230, - 210, - }), - charts.NewSeriesFromValues([]float64{ - 220, - 182, - 191, - 234, - 290, - 330, - 310, - }), - charts.NewSeriesFromValues([]float64{ - 150, - 232, - 201, - 154, - 190, - 330, - 410, - }), - charts.NewSeriesFromValues([]float64{ - 320, - 332, - 301, - 334, - 390, - 330, - 320, - }), - charts.NewSeriesFromValues([]float64{ - 820, - 932, - 901, - 934, - 1290, - 1330, - 1320, - }), - }, - }, - // 温度折线图 - { - Title: charts.TitleOption{ - Text: "Temperature Change in the Coming Week", - }, - Padding: chart.Box{ - Top: 20, - Left: 20, - Right: 30, - Bottom: 20, - }, - Legend: charts.NewLegendOption([]string{ - "Highest", - "Lowest", - }, charts.PositionRight), - XAxis: charts.NewXAxisOption([]string{ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun", - }, charts.FalseFlag()), - SeriesList: []charts.Series{ - { - Data: charts.NewSeriesDataFromValues([]float64{ - 14, - 11, - 13, - 11, - 12, - 12, - 7, - }), - MarkPoint: charts.NewMarkPoint(charts.SeriesMarkDataTypeMax, charts.SeriesMarkDataTypeMin), - MarkLine: charts.NewMarkLine(charts.SeriesMarkDataTypeAverage), - }, - { - Data: charts.NewSeriesDataFromValues([]float64{ - 1, - -2, - 2, - 5, - 3, - 2, - 0, - }), - MarkLine: charts.NewMarkLine(charts.SeriesMarkDataTypeAverage), - }, - }, - }, - // 柱状图 - { - Title: charts.TitleOption{ - Text: "Bar", - }, - XAxis: charts.NewXAxisOption([]string{ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun", - }), - Legend: charts.LegendOption{ - Data: []string{ - "Rainfall", - "Evaporation", - }, - Icon: charts.LegendIconRect, - }, - SeriesList: []charts.Series{ - charts.NewSeriesFromValues([]float64{ - 120, - 200, - 150, - 80, - 70, - 110, - 130, - }, charts.ChartTypeBar), - { - Type: charts.ChartTypeBar, - Data: []charts.SeriesData{ - { - Value: 100, - }, - { - Value: 190, - Style: chart.Style{ - FillColor: drawing.Color{ - R: 169, - G: 0, - B: 0, - A: 255, - }, - }, - }, - { - Value: 230, - }, - { - Value: 140, - }, - { - Value: 100, - }, - { - Value: 200, - }, - { - Value: 180, - }, - }, - }, - }, - }, - // 柱状图+mark - { - Title: charts.TitleOption{ - Text: "Rainfall vs Evaporation", - Subtext: "Fake Data", - }, - Padding: chart.Box{ - Top: 20, - Right: 20, - Bottom: 20, - Left: 20, - }, - XAxis: charts.NewXAxisOption([]string{ - "Jan", - "Feb", - "Mar", - "Apr", - "May", - "Jun", - "Jul", - "Aug", - "Sep", - "Oct", - "Nov", - "Dec", - }), - Legend: charts.NewLegendOption([]string{ - "Rainfall", - "Evaporation", - }, charts.PositionRight), - SeriesList: []charts.Series{ - { - Type: charts.ChartTypeBar, - Data: charts.NewSeriesDataFromValues([]float64{ - 2.0, - 4.9, - 7.0, - 23.2, - 25.6, - 76.7, - 135.6, - 162.2, - 32.6, - 20.0, - 6.4, - 3.3, - }), - MarkPoint: charts.NewMarkPoint( - charts.SeriesMarkDataTypeMax, - charts.SeriesMarkDataTypeMin, - ), - MarkLine: charts.NewMarkLine( - charts.SeriesMarkDataTypeAverage, - ), - }, - { - Type: charts.ChartTypeBar, - Data: charts.NewSeriesDataFromValues([]float64{ - 2.6, - 5.9, - 9.0, - 26.4, - 28.7, - 70.7, - 175.6, - 182.2, - 48.7, - 18.8, - 6.0, - 2.3, - }), - MarkPoint: charts.NewMarkPoint( - charts.SeriesMarkDataTypeMax, - charts.SeriesMarkDataTypeMin, - ), - MarkLine: charts.NewMarkLine( - charts.SeriesMarkDataTypeAverage, - ), - }, - }, - }, - // 双Y轴示例 - { - XAxis: charts.NewXAxisOption([]string{ - "Jan", - "Feb", - "Mar", - "Apr", - "May", - "Jun", - "Jul", - "Aug", - "Sep", - "Oct", - "Nov", - "Dec", - }), - Legend: charts.NewLegendOption([]string{ - "Evaporation", - "Precipitation", - "Temperature", - }), - YAxisList: []charts.YAxisOption{ - { - Formatter: "{value}°C", - Color: drawing.Color{ - R: 250, - G: 200, - B: 88, - A: 255, - }, - }, - { - Formatter: "{value}ml", - Color: drawing.Color{ - R: 84, - G: 112, - B: 198, - A: 255, - }, - }, - }, - SeriesList: []charts.Series{ - { - Type: charts.ChartTypeBar, - Data: charts.NewSeriesDataFromValues([]float64{ - 2.0, - 4.9, - 7.0, - 23.2, - 25.6, - 76.7, - 135.6, - 162.2, - 32.6, - 20.0, - 6.4, - 3.3, - 10.2, - }), - YAxisIndex: 1, - }, - { - Type: charts.ChartTypeBar, - Data: charts.NewSeriesDataFromValues([]float64{ - 2.6, - 5.9, - 9.0, - 26.4, - 28.7, - 70.7, - 175.6, - 182.2, - 48.7, - 18.8, - 6.0, - 2.3, - 20.2, - }), - YAxisIndex: 1, - }, - { - Data: charts.NewSeriesDataFromValues([]float64{ - 2.0, - 2.2, - 3.3, - 4.5, - 6.3, - 10.2, - 20.3, - 23.4, - 23.0, - 16.5, - 12.0, - 6.2, - 30.3, - }), - }, - }, - }, - // 饼图 - { - Title: charts.TitleOption{ - Text: "Referer of a Website", - Subtext: "Fake Data", - Left: charts.PositionCenter, - }, - Legend: charts.LegendOption{ - Orient: charts.OrientVertical, - Data: []string{ - "Search Engine", - "Direct", - "Email", - "Union Ads", - "Video Ads", - }, - Left: charts.PositionLeft, - }, - SeriesList: charts.NewPieSeriesList([]float64{ - 1048, - 735, - 580, - 484, - 300, - }, charts.PieSeriesOption{ - Label: charts.SeriesLabel{ - Show: true, - }, - Radius: "35%", - }), - }, - // 雷达图 - { - Title: charts.TitleOption{ - Text: "Basic Radar Chart", - }, - Legend: charts.NewLegendOption([]string{ - "Allocated Budget", - "Actual Spending", - }), - RadarIndicators: []charts.RadarIndicator{ - { - Name: "Sales", - Max: 6500, - }, - { - Name: "Administration", - Max: 16000, - }, - { - Name: "Information Technology", - Max: 30000, - }, - { - Name: "Customer Support", - Max: 38000, - }, - { - Name: "Development", - Max: 52000, - }, - { - Name: "Marketing", - Max: 25000, - }, - }, - SeriesList: charts.SeriesList{ - { - Type: charts.ChartTypeRadar, - Data: charts.NewSeriesDataFromValues([]float64{ - 4200, - 3000, - 20000, - 35000, - 50000, - 18000, - }), - }, - { - Type: charts.ChartTypeRadar, - Data: charts.NewSeriesDataFromValues([]float64{ - 5000, - 14000, - 28000, - 26000, - 42000, - 21000, - }), - }, - }, - }, - // 漏斗图 - { - Title: charts.TitleOption{ - Text: "Funnel", - }, - Legend: charts.NewLegendOption([]string{ - "Show", - "Click", - "Visit", - "Inquiry", - "Order", - }), - SeriesList: []charts.Series{ - { - Type: charts.ChartTypeFunnel, - Name: "Visit", - Data: charts.NewSeriesDataFromValues([]float64{ - 60, - }), - }, - { - Type: charts.ChartTypeFunnel, - Name: "Inquiry", - Data: charts.NewSeriesDataFromValues([]float64{ - 40, - }), - }, - { - Type: charts.ChartTypeFunnel, - Name: "Order", - Data: charts.NewSeriesDataFromValues([]float64{ - 20, - }), - }, - { - Type: charts.ChartTypeFunnel, - Name: "Click", - Data: charts.NewSeriesDataFromValues([]float64{ - 80, - }), - }, - { - Type: charts.ChartTypeFunnel, - Name: "Show", - Data: charts.NewSeriesDataFromValues([]float64{ - 100, - }), - }, - }, - }, - // 多图展示 - { - Legend: charts.LegendOption{ - Top: "-90", - Data: []string{ - "Milk Tea", - "Matcha Latte", - "Cheese Cocoa", - "Walnut Brownie", - }, - }, - Padding: chart.Box{ - Top: 100, - Right: 10, - Bottom: 10, - Left: 10, - }, - XAxis: charts.NewXAxisOption([]string{ - "2012", - "2013", - "2014", - "2015", - "2016", - "2017", - }), - YAxisList: []charts.YAxisOption{ - { - - Min: charts.NewFloatPoint(0), - Max: charts.NewFloatPoint(90), - }, - }, - SeriesList: []charts.Series{ - charts.NewSeriesFromValues([]float64{ - 56.5, - 82.1, - 88.7, - 70.1, - 53.4, - 85.1, - }), - charts.NewSeriesFromValues([]float64{ - 51.1, - 51.4, - 55.1, - 53.3, - 73.8, - 68.7, - }), - charts.NewSeriesFromValues([]float64{ - 40.1, - 62.2, - 69.5, - 36.4, - 45.2, - 32.5, - }, charts.ChartTypeBar), - charts.NewSeriesFromValues([]float64{ - 25.2, - 37.1, - 41.2, - 18, - 33.9, - 49.1, - }, charts.ChartTypeBar), - }, - Children: []charts.ChartOption{ - { - Legend: charts.LegendOption{ - Show: charts.FalseFlag(), - Data: []string{ - "Milk Tea", - "Matcha Latte", - "Cheese Cocoa", - "Walnut Brownie", - }, - }, - Box: chart.Box{ - Top: 20, - Left: 400, - Right: 500, - Bottom: 120, - }, - SeriesList: charts.NewPieSeriesList([]float64{ - 435.9, - 354.3, - 285.9, - 204.5, - }, charts.PieSeriesOption{ - Label: charts.SeriesLabel{ - Show: true, - }, - Radius: "35%", - }), - }, - }, - }, - } - handler(w, req, chartOptions, nil) -} - -func echartsHandler(w http.ResponseWriter, req *http.Request) { - echartsOptions := []string{ - `{ - "xAxis": { - "type": "category", - "data": [ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun" - ] - }, - "yAxis": { - "type": "value" - }, - "series": [ - { - "data": [ - 150, - 230, - 224, - 218, - 135, - 147, - 260 - ], - "type": "line" - } - ] - }`, - `{ - "title": { - "text": "Multiple Line" - }, - "tooltip": { - "trigger": "axis" - }, - "legend": { - "left": "right", - "data": [ - "Email", - "Union Ads", - "Video Ads", - "Direct", - "Search Engine" - ] - }, - "grid": { - "left": "3%", - "right": "4%", - "bottom": "3%", - "containLabel": true - }, - "toolbox": { - "feature": { - "saveAsImage": {} - } - }, - "xAxis": { - "type": "category", - "boundaryGap": false, - "data": [ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun" - ] - }, - "yAxis": { - "type": "value" - }, - "series": [ - { - "name": "Email", - "type": "line", - "data": [ - 120, - 132, - 101, - 134, - 90, - 230, - 210 - ] - }, - { - "name": "Union Ads", - "type": "line", - "data": [ - 220, - 182, - 191, - 234, - 290, - 330, - 310 - ] - }, - { - "name": "Video Ads", - "type": "line", - "data": [ - 150, - 232, - 201, - 154, - 190, - 330, - 410 - ] - }, - { - "name": "Direct", - "type": "line", - "data": [ - 320, - 332, - 301, - 334, - 390, - 330, - 320 - ] - }, - { - "name": "Search Engine", - "type": "line", - "data": [ - 820, - 932, - 901, - 934, - 1290, - 1330, - 1320 - ] - } - ] - }`, - `{ - "title": { - "text": "Temperature Change in the Coming Week" - }, - "legend": { - "left": "right" - }, - "padding": [10, 30, 10, 10], - "xAxis": { - "type": "category", - "boundaryGap": false, - "data": [ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun" - ] - }, - "yAxis": { - "axisLabel": { - "formatter": "{value} °C" - } - }, - "series": [ - { - "name": "Highest", - "type": "line", - "data": [ - 10, - 11, - 13, - 11, - 12, - 12, - 9 - ], - "markPoint": { - "data": [ - { - "type": "max" - }, - { - "type": "min" - } - ] - }, - "markLine": { - "data": [ - { - "type": "average" - } - ] - } - }, - { - "name": "Lowest", - "type": "line", - "data": [ - 1, - -2, - 2, - 5, - 3, - 2, - 0 - ], - "markPoint": { - "data": [ - { - "type": "min" - } - ] - }, - "markLine": { - "data": [ - { - "type": "average" - }, - { - "type": "max" - } - ] - } - } - ] - }`, - `{ - "xAxis": { - "type": "category", - "data": [ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun" - ] - }, - "yAxis": { - "type": "value" - }, - "series": [ - { - "data": [ - 120, - 200, - 150, - 80, - 70, - 110, - 130 - ], - "type": "bar" - } - ] - }`, - `{ - "xAxis": { - "type": "category", - "data": [ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun" - ] - }, - "yAxis": { - "type": "value" - }, - "series": [ - { - "data": [ - 120, - { - "value": 200, - "itemStyle": { - "color": "#a90000" - } - }, - 150, - 80, - 70, - 110, - 130 - ], - "type": "bar" - } - ] - }`, - `{ - "title": { - "text": "Rainfall vs Evaporation", - "subtext": "Fake Data" - }, - "legend": { - "data": [ - "Rainfall", - "Evaporation" - ] - }, - "padding": [10, 30, 10, 10], - "xAxis": [ - { - "type": "category", - "data": [ - "Jan", - "Feb", - "Mar", - "Apr", - "May", - "Jun", - "Jul", - "Aug", - "Sep", - "Oct", - "Nov", - "Dec" - ] - } - ], - "series": [ - { - "name": "Rainfall", - "type": "bar", - "data": [ - 2, - 4.9, - 7, - 23.2, - 25.6, - 76.7, - 135.6, - 162.2, - 32.6, - 20, - 6.4, - 3.3 - ], - "markPoint": { - "data": [ - { - "type": "max" - }, - { - "type": "min" - } - ] - }, - "markLine": { - "data": [ - { - "type": "average" - } - ] - } - }, - { - "name": "Evaporation", - "type": "bar", - "data": [ - 2.6, - 5.9, - 9, - 26.4, - 28.7, - 70.7, - 175.6, - 182.2, - 48.7, - 18.8, - 6, - 2.3 - ], - "markPoint": { - "data": [ - { - "type": "max" - }, - { - "type": "min" - } - ] - }, - "markLine": { - "data": [ - { - "type": "average" - } - ] - } - } - ] - }`, - `{ - "legend": { - "data": [ - "Evaporation", - "Precipitation", - "Temperature" - ] - }, - "xAxis": [ - { - "type": "category", - "data": [ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun" - ], - "axisPointer": { - "type": "shadow" - } - } - ], - "yAxis": [ - { - "type": "value", - "name": "Precipitation", - "min": 0, - "max": 240, - "axisLabel": { - "formatter": "{value} ml" - } - }, - { - "type": "value", - "name": "Temperature", - "min": 0, - "max": 24, - "axisLabel": { - "formatter": "{value} °C" - } - } - ], - "series": [ - { - "name": "Evaporation", - "type": "bar", - "tooltip": {}, - "data": [ - 2, - 4.9, - 7, - 23.2, - 25.6, - 76.7, - 135.6, - 162.2, - 32.6, - 20, - 6.4, - 3.3 - ] - }, - { - "name": "Precipitation", - "type": "bar", - "tooltip": {}, - "data": [ - 2.6, - 5.9, - 9, - 26.4, - 28.7, - 70.7, - 175.6, - 182.2, - 48.7, - 18.8, - 6, - 2.3 - ] - }, - { - "name": "Temperature", - "type": "line", - "yAxisIndex": 1, - "tooltip": {}, - "data": [ - 2, - 2.2, - 3.3, - 4.5, - 6.3, - 10.2, - 20.3, - 23.4, - 23, - 16.5, - 12, - 6.2 - ] - } - ] - }`, - `{ - "tooltip": { - "trigger": "axis", - "axisPointer": { - "type": "cross" - } - }, - "grid": { - "right": "20%" - }, - "toolbox": { - "feature": { - "dataView": { - "show": true, - "readOnly": false - }, - "restore": { - "show": true - }, - "saveAsImage": { - "show": true - } - } - }, - "legend": { - "data": [ - "Evaporation", - "Precipitation", - "Temperature" - ] - }, - "xAxis": [ - { - "type": "category", - "axisTick": { - "alignWithLabel": true - }, - "data": [ - "Jan", - "Feb", - "Mar", - "Apr", - "May", - "Jun", - "Jul", - "Aug", - "Sep", - "Oct", - "Nov", - "Dec" - ] - } - ], - "yAxis": [ - { - "type": "value", - "name": "温度", - "position": "left", - "alignTicks": true, - "axisLine": { - "show": true, - "lineStyle": { - "color": "#EE6666" - } - }, - "axisLabel": { - "formatter": "{value} °C" - } - }, - { - "type": "value", - "name": "Evaporation", - "position": "right", - "alignTicks": true, - "axisLine": { - "show": true, - "lineStyle": { - "color": "#5470C6" - } - }, - "axisLabel": { - "formatter": "{value} ml" - } - } - ], - "series": [ - { - "name": "Evaporation", - "type": "bar", - "yAxisIndex": 1, - "data": [ - 2, - 4.9, - 7, - 23.2, - 25.6, - 76.7, - 135.6, - 162.2, - 32.6, - 20, - 6.4, - 3.3 - ] - }, - { - "name": "Precipitation", - "type": "bar", - "yAxisIndex": 1, - "data": [ - 2.6, - 5.9, - 9, - 26.4, - 28.7, - 70.7, - 175.6, - 182.2, - 48.7, - 18.8, - 6, - 2.3 - ] - }, - { - "name": "Temperature", - "type": "line", - "data": [ - 2, - 2.2, - 3.3, - 4.5, - 6.3, - 10.2, - 20.3, - 23.4, - 23, - 16.5, - 12, - 6.2 - ] - } - ] - }`, - `{ - "title": { - "text": "Referer of a Website", - "subtext": "Fake Data", - "left": "center" - }, - "tooltip": { - "trigger": "item" - }, - "legend": { - "orient": "vertical", - "left": "left" - }, - "series": [ - { - "name": "Access From", - "type": "pie", - "radius": "50%", - "data": [ - { - "value": 1048, - "name": "Search Engine" - }, - { - "value": 735, - "name": "Direct" - }, - { - "value": 580, - "name": "Email" - }, - { - "value": 484, - "name": "Union Ads" - }, - { - "value": 300, - "name": "Video Ads" - } - ] - } - ] - }`, - `{ - "title": { - "text": "Rainfall" - }, - "padding": [10, 10, 10, 30], - "legend": { - "data": [ - "GZ", - "SH" - ] - }, - "xAxis": { - "type": "category", - "splitNumber": 6, - "data": [ - "01-01", - "01-02", - "01-03", - "01-04", - "01-05", - "01-06", - "01-07", - "01-08", - "01-09", - "01-10", - "01-11", - "01-12", - "01-13", - "01-14", - "01-15", - "01-16", - "01-17", - "01-18", - "01-19", - "01-20", - "01-21", - "01-22", - "01-23", - "01-24", - "01-25", - "01-26", - "01-27", - "01-28", - "01-29", - "01-30", - "01-31" - ] - }, - "yAxis": { - "axisLabel": { - "formatter": "{value} mm" - } - }, - "series": [ - { - "type": "bar", - "data": [ - 928, - 821, - 889, - 600, - 547, - 783, - 197, - 853, - 430, - 346, - 63, - 465, - 309, - 334, - 141, - 538, - 792, - 58, - 922, - 807, - 298, - 243, - 744, - 885, - 812, - 231, - 330, - 220, - 984, - 221, - 429 - ] - }, - { - "type": "bar", - "data": [ - 749, - 201, - 296, - 579, - 255, - 159, - 902, - 246, - 149, - 158, - 507, - 776, - 186, - 79, - 390, - 222, - 601, - 367, - 221, - 411, - 714, - 620, - 966, - 73, - 203, - 631, - 833, - 610, - 487, - 677, - 596 - ] - } - ] - }`, - `{ - "title": { - "text": "Basic Radar Chart" - }, - "legend": { - "data": [ - "Allocated Budget", - "Actual Spending" - ] - }, - "radar": { - "indicator": [ - { - "name": "Sales", - "max": 6500 - }, - { - "name": "Administration", - "max": 16000 - }, - { - "name": "Information Technology", - "max": 30000 - }, - { - "name": "Customer Support", - "max": 38000 - }, - { - "name": "Development", - "max": 52000 - }, - { - "name": "Marketing", - "max": 25000 - } - ] - }, - "series": [ - { - "name": "Budget vs spending", - "type": "radar", - "data": [ - { - "value": [ - 4200, - 3000, - 20000, - 35000, - 50000, - 18000 - ], - "name": "Allocated Budget" - }, - { - "value": [ - 5000, - 14000, - 28000, - 26000, - 42000, - 21000 - ], - "name": "Actual Spending" - } - ] - } - ] - }`, - `{ - "title": { - "text": "Funnel" - }, - "tooltip": { - "trigger": "item", - "formatter": "{a}
{b} : {c}%" - }, - "toolbox": { - "feature": { - "dataView": { - "readOnly": false - }, - "restore": {}, - "saveAsImage": {} - } - }, - "legend": { - "data": [ - "Show", - "Click", - "Visit", - "Inquiry", - "Order" - ] - }, - "series": [ - { - "name": "Funnel", - "type": "funnel", - "left": "10%", - "top": 60, - "bottom": 60, - "width": "80%", - "min": 0, - "max": 100, - "minSize": "0%", - "maxSize": "100%", - "sort": "descending", - "gap": 2, - "label": { - "show": true, - "position": "inside" - }, - "labelLine": { - "length": 10, - "lineStyle": { - "width": 1, - "type": "solid" - } - }, - "itemStyle": { - "borderColor": "#fff", - "borderWidth": 1 - }, - "emphasis": { - "label": { - "fontSize": 20 - } - }, - "data": [ - { - "value": 60, - "name": "Visit" - }, - { - "value": 40, - "name": "Inquiry" - }, - { - "value": 20, - "name": "Order" - }, - { - "value": 80, - "name": "Click" - }, - { - "value": 100, - "name": "Show" - } - ] - } - ] - }`, - `{ - "legend": { - "top": "-140", - "data": [ - "Milk Tea", - "Matcha Latte", - "Cheese Cocoa", - "Walnut Brownie" - ] - }, - "padding": [ - 150, - 10, - 10, - 10 - ], - "xAxis": [ - { - "data": [ - "2012", - "2013", - "2014", - "2015", - "2016", - "2017" - ] - } - ], - "series": [ - { - "data": [ - 56.5, - 82.1, - 88.7, - 70.1, - 53.4, - 85.1 - ] - }, - { - "data": [ - 51.1, - 51.4, - 55.1, - 53.3, - 73.8, - 68.7 - ] - }, - { - "data": [ - 40.1, - 62.2, - 69.5, - 36.4, - 45.2, - 32.5 - ] - }, - { - "data": [ - 25.2, - 37.1, - 41.2, - 18, - 33.9, - 49.1 - ] - } - ], - "children": [ - { - "box": { - "left": 0, - "top": 30, - "right": 600, - "bottom": 150 - }, - "legend": { - "show": false - }, - "series": [ - { - "type": "pie", - "radius": "50%", - "data": [ - { - "value": 435.9, - "name": "Milk Tea" - }, - { - "value": 354.3, - "name": "Matcha Latte" - }, - { - "value": 285.9, - "name": "Cheese Cocoa" - }, - { - "value": 204.5, - "name": "Walnut Brownie" - } - ] - } - ] - } - ] - }`, - } - handler(w, req, nil, echartsOptions) -} - -func main() { - http.HandleFunc("/", indexHandler) - http.HandleFunc("/echarts", echartsHandler) - http.ListenAndServe(":3012", nil) -} diff --git a/funnel.go b/funnel.go deleted file mode 100644 index f083306..0000000 --- a/funnel.go +++ /dev/null @@ -1,141 +0,0 @@ -// 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 ( - "fmt" - "sort" - - "github.com/dustin/go-humanize" - "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2" -) - -type funnelChartOption struct { - Theme string - Font *truetype.Font - SeriesList SeriesList -} - -func funnelChartRender(opt funnelChartOption, result *basicRenderResult) error { - d, err := NewDraw(DrawOption{ - Parent: result.d, - }, PaddingOption(chart.Box{ - Top: result.titleBox.Height(), - })) - if err != nil { - return err - } - seriesList := make([]Series, len(opt.SeriesList)) - copy(seriesList, opt.SeriesList) - sort.Slice(seriesList, func(i, j int) bool { - // 大的数据在前 - return seriesList[i].Data[0].Value > seriesList[j].Data[0].Value - }) - max := seriesList[0].Data[0].Value - min := float64(0) - for _, item := range seriesList { - if item.Max != nil { - max = *item.Max - } - if item.Min != nil { - min = *item.Min - } - } - - theme := NewTheme(opt.Theme) - gap := 2 - height := d.Box.Height() - width := d.Box.Width() - count := len(seriesList) - - h := (height - gap*(count-1)) / count - - y := 0 - widthList := make([]int, len(seriesList)) - textList := make([]string, len(seriesList)) - for index, item := range seriesList { - value := item.Data[0].Value - widthPercent := (value - min) / (max - min) - w := int(widthPercent * float64(width)) - widthList[index] = w - p := humanize.CommafWithDigits(value/max*100, 2) + "%" - textList[index] = fmt.Sprintf("%s(%s)", item.Name, p) - } - - for index, w := range widthList { - series := seriesList[index] - nextWidth := 0 - if index+1 < len(widthList) { - nextWidth = widthList[index+1] - } - topStartX := (width - w) >> 1 - topEndX := topStartX + w - bottomStartX := (width - nextWidth) >> 1 - bottomEndX := bottomStartX + nextWidth - points := []Point{ - { - X: topStartX, - Y: y, - }, - { - X: topEndX, - Y: y, - }, - { - X: bottomEndX, - Y: y + h, - }, - { - X: bottomStartX, - Y: y + h, - }, - { - X: topStartX, - Y: y, - }, - } - color := theme.GetSeriesColor(series.index) - d.fill(points, chart.Style{ - FillColor: color, - }) - - // 文本 - text := textList[index] - r := d.Render - textStyle := chart.Style{ - FontColor: theme.GetTextColor(), - FontSize: labelFontSize, - Font: opt.Font, - } - textStyle.GetTextOptions().WriteToRenderer(r) - textBox := r.MeasureText(text) - textX := width>>1 - textBox.Width()>>1 - textY := y + h>>1 - d.text(text, textX, textY) - - y += (h + gap) - } - - return nil -} diff --git a/funnel_test.go b/funnel_test.go deleted file mode 100644 index 530fa53..0000000 --- a/funnel_test.go +++ /dev/null @@ -1,91 +0,0 @@ -// 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" - "github.com/wcharczuk/go-chart/v2" -) - -func TestFunnelChartRender(t *testing.T) { - assert := assert.New(t) - - d, err := NewDraw(DrawOption{ - Width: 250, - Height: 150, - }) - assert.Nil(err) - f, _ := chart.GetDefaultFont() - err = funnelChartRender(funnelChartOption{ - Font: f, - SeriesList: []Series{ - { - Type: ChartTypeFunnel, - Name: "Visit", - Data: NewSeriesDataFromValues([]float64{ - 60, - }), - }, - { - Type: ChartTypeFunnel, - Name: "Inquiry", - Data: NewSeriesDataFromValues([]float64{ - 40, - }), - index: 1, - }, - { - Type: ChartTypeFunnel, - Name: "Order", - Data: NewSeriesDataFromValues([]float64{ - 20, - }), - index: 2, - }, - { - Type: ChartTypeFunnel, - Name: "Click", - Data: NewSeriesDataFromValues([]float64{ - 80, - }), - index: 3, - }, - { - Type: ChartTypeFunnel, - Name: "Show", - Data: NewSeriesDataFromValues([]float64{ - 100, - }), - index: 4, - }, - }, - }, &basicRenderResult{ - d: d, - }) - assert.Nil(err) - data, err := d.Bytes() - assert.Nil(err) - assert.Equal("\\nShow(100%)Click(80%)Visit(60%)Inquiry(40%)Order(20%)", string(data)) -} diff --git a/legend.go b/legend.go deleted file mode 100644 index df72757..0000000 --- a/legend.go +++ /dev/null @@ -1,226 +0,0 @@ -// 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 LegendOption struct { - theme string - // Legend show flag, if nil or true, the legend will be shown - Show *bool - // Legend text style - Style chart.Style - // Text array of legend - Data []string - // Distance between legend component and the left side of the container. - // It can be pixel value: 20, percentage value: 20%, - // or position value: right, center. - Left string - // Distance between legend component and the top side of the container. - // It can be pixel value: 20. - Top string - // Legend marker and text aligning, it can be left or right, default is left. - Align string - // The layout orientation of legend, it can be horizontal or vertical, default is horizontal. - Orient string - // Icon of the legend. - Icon string -} - -const ( - LegendIconRect = "rect" -) - -// NewLegendOption creates a new legend option by legend text list -func NewLegendOption(data []string, position ...string) LegendOption { - opt := LegendOption{ - Data: data, - } - if len(position) != 0 { - opt.Left = position[0] - } - return opt -} - -type legend struct { - d *Draw - opt *LegendOption -} - -func NewLegend(d *Draw, opt LegendOption) *legend { - return &legend{ - d: d, - opt: &opt, - } -} - -func (l *legend) Render() (chart.Box, error) { - d := l.d - opt := l.opt - if len(opt.Data) == 0 || isFalse(opt.Show) { - return chart.BoxZero, nil - } - theme := NewTheme(opt.theme) - padding := opt.Style.Padding - legendDraw, err := NewDraw(DrawOption{ - Parent: d, - }, PaddingOption(padding)) - if err != nil { - return chart.BoxZero, err - } - r := legendDraw.Render - opt.Style.GetTextOptions().WriteToRenderer(r) - - x := 0 - y := 0 - top := 0 - // TODO TOP 暂只支持数值 - if opt.Top != "" { - top, _ = strconv.Atoi(opt.Top) - y += top - } - legendWidth := 30 - legendDotHeight := 5 - textPadding := 5 - legendMargin := 10 - // 往下移2倍dot的高度 - y += 2 * legendDotHeight - - widthCount := 0 - maxTextWidth := 0 - // 文本宽度 - for _, text := range opt.Data { - b := r.MeasureText(text) - if b.Width() > maxTextWidth { - maxTextWidth = b.Width() - } - widthCount += b.Width() - } - if opt.Orient == OrientVertical { - widthCount = maxTextWidth + legendWidth + textPadding - } else { - // 加上标记 - widthCount += legendWidth * len(opt.Data) - // 文本的padding - widthCount += 2 * textPadding * len(opt.Data) - // margin的宽度 - widthCount += legendMargin * (len(opt.Data) - 1) - } - - left := 0 - switch opt.Left { - case PositionRight: - left = legendDraw.Box.Width() - widthCount - case PositionCenter: - left = (legendDraw.Box.Width() - widthCount) >> 1 - default: - if strings.HasSuffix(opt.Left, "%") { - value, _ := strconv.Atoi(strings.ReplaceAll(opt.Left, "%", "")) - left = legendDraw.Box.Width() * value / 100 - } else { - value, _ := strconv.Atoi(opt.Left) - left = value - } - } - x = left - for index, text := range opt.Data { - textBox := r.MeasureText(text) - var renderText func() - if opt.Orient == OrientVertical { - // 垂直 - // 重置x的位置 - x = left - renderText = func() { - x += textPadding - legendDraw.text(text, x, y+legendDotHeight) - x += textBox.Width() - y += (2*legendDotHeight + legendMargin) - } - - } else { - // 水平 - if index != 0 { - x += legendMargin - } - renderText = func() { - x += textPadding - legendDraw.text(text, x, y+legendDotHeight) - x += textBox.Width() - x += textPadding - } - } - if opt.Align == PositionRight { - renderText() - } - seriesColor := theme.GetSeriesColor(index) - fillColor := seriesColor - if !theme.IsDark() { - fillColor = theme.GetBackgroundColor() - } - style := chart.Style{ - StrokeColor: seriesColor, - FillColor: fillColor, - StrokeWidth: 3, - } - if opt.Icon == LegendIconRect { - style.FillColor = seriesColor - style.StrokeWidth = 1 - } - style.GetFillAndStrokeOptions().WriteDrawingOptionsToRenderer(r) - - if opt.Icon == LegendIconRect { - legendDraw.moveTo(x, y-legendDotHeight) - legendDraw.lineTo(x+legendWidth, y-legendDotHeight) - legendDraw.lineTo(x+legendWidth, y+legendDotHeight) - legendDraw.lineTo(x, y+legendDotHeight) - legendDraw.lineTo(x, y-legendDotHeight) - r.FillStroke() - } else { - legendDraw.moveTo(x, y) - legendDraw.lineTo(x+legendWidth, y) - r.Stroke() - legendDraw.circle(float64(legendDotHeight), x+legendWidth>>1, y) - r.FillStroke() - } - x += legendWidth - - if opt.Align != PositionRight { - renderText() - } - } - legendBox := padding.Clone() - // 计算展示区域 - if opt.Orient == OrientVertical { - legendBox.Right = legendBox.Left + left + maxTextWidth + legendWidth + textPadding - legendBox.Bottom = legendBox.Top + y - } else { - legendBox.Right = legendBox.Left + x - legendBox.Bottom = legendBox.Top + 2*legendDotHeight + top + textPadding - } - return legendBox, nil -} diff --git a/legend_test.go b/legend_test.go deleted file mode 100644 index c5d7e50..0000000 --- a/legend_test.go +++ /dev/null @@ -1,185 +0,0 @@ -// 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" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" -) - -func TestNewLegendOption(t *testing.T) { - assert := assert.New(t) - - opt := NewLegendOption([]string{ - "a", - "b", - }, PositionRight) - assert.Equal(LegendOption{ - Data: []string{ - "a", - "b", - }, - Left: PositionRight, - }, opt) -} - -func TestLegendRender(t *testing.T) { - assert := assert.New(t) - - newDraw := func() *Draw { - d, _ := NewDraw(DrawOption{ - Width: 400, - Height: 300, - }) - return d - } - style := chart.Style{ - FontSize: 10, - FontColor: drawing.ColorBlack, - } - style.Font, _ = chart.GetDefaultFont() - - tests := []struct { - newDraw func() *Draw - newLegend func(*Draw) *legend - box chart.Box - result string - }{ - { - newDraw: newDraw, - newLegend: func(d *Draw) *legend { - return NewLegend(d, LegendOption{ - Top: "10", - Data: []string{ - "Mon", - "Tue", - "Wed", - }, - Style: style, - }) - }, - result: "\\nMonTueWed", - box: chart.Box{ - Right: 214, - Bottom: 25, - }, - }, - { - newDraw: newDraw, - newLegend: func(d *Draw) *legend { - return NewLegend(d, LegendOption{ - Top: "10", - Left: PositionRight, - Align: PositionRight, - Data: []string{ - "Mon", - "Tue", - "Wed", - }, - Style: style, - }) - }, - result: "\\nMonTueWed", - box: chart.Box{ - Right: 400, - Bottom: 25, - }, - }, - { - newDraw: newDraw, - newLegend: func(d *Draw) *legend { - return NewLegend(d, LegendOption{ - Top: "10", - Left: PositionCenter, - Data: []string{ - "Mon", - "Tue", - "Wed", - }, - Style: style, - }) - }, - result: "\\nMonTueWed", - box: chart.Box{ - Right: 307, - Bottom: 25, - }, - }, - { - newDraw: newDraw, - newLegend: func(d *Draw) *legend { - return NewLegend(d, LegendOption{ - Top: "10", - Left: PositionLeft, - Data: []string{ - "Mon", - "Tue", - "Wed", - }, - Style: style, - Orient: OrientVertical, - }) - }, - result: "\\nMonTueWed", - box: chart.Box{ - Right: 61, - Bottom: 80, - }, - }, - { - newDraw: newDraw, - newLegend: func(d *Draw) *legend { - return NewLegend(d, LegendOption{ - Top: "10", - Left: "10%", - Data: []string{ - "Mon", - "Tue", - "Wed", - }, - Style: style, - Orient: OrientVertical, - }) - }, - box: chart.Box{ - Right: 101, - Bottom: 80, - }, - result: "\\nMonTueWed", - }, - } - - for _, tt := range tests { - d := tt.newDraw() - b, err := tt.newLegend(d).Render() - assert.Nil(err) - assert.Equal(tt.box, b) - data, err := d.Bytes() - assert.Nil(err) - assert.NotEmpty(data) - assert.Equal(tt.result, string(data)) - } -} diff --git a/line.go b/line.go index 15ab575..e4b1f18 100644 --- a/line.go +++ b/line.go @@ -22,10 +22,6 @@ package charts -import ( - "github.com/wcharczuk/go-chart/v2" -) - type LineStyle struct { ClassName string StrokeDashArray []float64 @@ -37,8 +33,8 @@ type LineStyle struct { DotFillColor Color } -func (ls *LineStyle) Style() chart.Style { - return chart.Style{ +func (ls *LineStyle) Style() Style { + return Style{ ClassName: ls.ClassName, StrokeDashArray: ls.StrokeDashArray, StrokeColor: ls.StrokeColor, @@ -48,55 +44,3 @@ func (ls *LineStyle) Style() chart.Style { DotColor: ls.DotColor, } } - -func (d *Draw) lineFill(points []Point, style LineStyle) { - s := style.Style() - if !(s.ShouldDrawStroke() && s.ShouldDrawFill()) { - return - } - - newPoints := make([]Point, len(points)) - copy(newPoints, points) - x0 := points[0].X - y0 := points[0].Y - height := d.Box.Height() - newPoints = append(newPoints, Point{ - X: points[len(points)-1].X, - Y: height, - }, Point{ - X: x0, - Y: height, - }, Point{ - X: x0, - Y: y0, - }) - d.fill(newPoints, style.Style()) -} - -func (d *Draw) lineDot(points []Point, style LineStyle) { - s := style.Style() - if !s.ShouldDrawDot() { - return - } - r := d.Render - dotWith := s.GetDotWidth() - - s.GetDotOptions().WriteDrawingOptionsToRenderer(r) - for _, point := range points { - if !style.DotFillColor.IsZero() { - r.SetFillColor(style.DotFillColor) - } - r.SetStrokeColor(s.DotColor) - d.circle(dotWith, point.X, point.Y) - r.FillStroke() - } -} - -func (d *Draw) Line(points []Point, style LineStyle) { - if len(points) == 0 { - return - } - d.lineFill(points, style) - d.lineStroke(points, style) - d.lineDot(points, style) -} diff --git a/line_chart.go b/line_chart.go deleted file mode 100644 index ac9091c..0000000 --- a/line_chart.go +++ /dev/null @@ -1,131 +0,0 @@ -// 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" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" -) - -type lineChartOption struct { - Theme string - SeriesList SeriesList - Font *truetype.Font -} - -func lineChartRender(opt lineChartOption, result *basicRenderResult) ([]markPointRenderOption, error) { - - theme := NewTheme(opt.Theme) - - d, err := NewDraw(DrawOption{ - Parent: result.d, - }, PaddingOption(chart.Box{ - Top: result.titleBox.Height(), - Left: YAxisWidth, - })) - if err != nil { - return nil, err - } - seriesNames := opt.SeriesList.Names() - - r := d.Render - xRange := result.xRange - markPointRenderOptions := make([]markPointRenderOption, 0) - for i, s := range opt.SeriesList { - // 由于series是for range,为同一个数据,因此需要clone - // 后续需要使用,如mark point - series := s - index := series.index - if index == 0 { - index = i - } - seriesColor := theme.GetSeriesColor(index) - - yRange := result.getYRange(series.YAxisIndex) - points := make([]Point, 0, len(series.Data)) - // mark line - markLineRender(markLineRenderOption{ - Draw: d, - FillColor: seriesColor, - FontColor: theme.GetTextColor(), - StrokeColor: seriesColor, - Font: opt.Font, - Series: &series, - Range: yRange, - }) - - for j, item := range series.Data { - if j >= xRange.divideCount { - continue - } - y := yRange.getRestHeight(item.Value) - x := xRange.getWidth(float64(j)) - points = append(points, Point{ - Y: y, - X: x, - }) - if !series.Label.Show { - continue - } - distance := series.Label.Distance - if distance == 0 { - distance = 5 - } - text := NewValueLabelFormater(seriesNames, series.Label.Formatter)(i, item.Value, -1) - labelStyle := chart.Style{ - FontColor: theme.GetTextColor(), - FontSize: labelFontSize, - Font: opt.Font, - } - if !series.Label.Color.IsZero() { - labelStyle.FontColor = series.Label.Color - } - labelStyle.GetTextOptions().WriteToRenderer(r) - textBox := r.MeasureText(text) - d.text(text, x-textBox.Width()>>1, y-distance) - } - - dotFillColor := drawing.ColorWhite - if theme.IsDark() { - dotFillColor = seriesColor - } - d.Line(points, LineStyle{ - StrokeColor: seriesColor, - StrokeWidth: 2, - DotColor: seriesColor, - DotWidth: defaultDotWidth, - DotFillColor: dotFillColor, - }) - // draw mark point - markPointRenderOptions = append(markPointRenderOptions, markPointRenderOption{ - Draw: d, - FillColor: seriesColor, - Font: opt.Font, - Points: points, - Series: &series, - }) - } - - return markPointRenderOptions, nil -} diff --git a/line_chart_test.go b/line_chart_test.go deleted file mode 100644 index 9f5d9af..0000000 --- a/line_chart_test.go +++ /dev/null @@ -1,97 +0,0 @@ -// 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" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" -) - -func TestLineChartRender(t *testing.T) { - assert := assert.New(t) - - width := 400 - height := 300 - d, err := NewDraw(DrawOption{ - Width: width, - Height: height, - }) - assert.Nil(err) - - result := basicRenderResult{ - xRange: &Range{ - Min: 0, - Max: 4, - divideCount: 4, - Size: width, - Boundary: true, - }, - yRangeList: []*Range{ - { - divideCount: 6, - Max: 100, - Min: 0, - Size: height, - }, - }, - d: d, - } - f, _ := chart.GetDefaultFont() - _, err = lineChartRender(lineChartOption{ - Font: f, - SeriesList: SeriesList{ - { - Label: SeriesLabel{ - Show: true, - Color: drawing.ColorBlue, - }, - MarkLine: NewMarkLine( - SeriesMarkDataTypeAverage, - ), - Data: []SeriesData{ - { - Value: 20, - }, - { - Value: 60, - }, - { - Value: 90, - }, - }, - }, - NewSeriesFromValues([]float64{ - 40, - 60, - 70, - }), - }, - }, &result) - assert.Nil(err) - data, err := d.Bytes() - assert.Nil(err) - assert.Equal("\\n56.66206090", string(data)) -} diff --git a/line_test.go b/line_test.go deleted file mode 100644 index e10b806..0000000 --- a/line_test.go +++ /dev/null @@ -1,165 +0,0 @@ -// 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" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" -) - -func TestLineStyle(t *testing.T) { - assert := assert.New(t) - - ls := LineStyle{ - ClassName: "test", - StrokeDashArray: []float64{ - 1.0, - }, - StrokeColor: drawing.ColorBlack, - StrokeWidth: 1, - FillColor: drawing.ColorBlack.WithAlpha(60), - DotWidth: 2, - DotColor: drawing.ColorBlack, - DotFillColor: drawing.ColorWhite, - } - - assert.Equal(chart.Style{ - ClassName: "test", - StrokeDashArray: []float64{ - 1.0, - }, - StrokeColor: drawing.ColorBlack, - StrokeWidth: 1, - FillColor: drawing.ColorBlack.WithAlpha(60), - DotWidth: 2, - DotColor: drawing.ColorBlack, - }, ls.Style()) -} - -func TestDrawLineFill(t *testing.T) { - assert := assert.New(t) - - ls := LineStyle{ - StrokeColor: drawing.ColorBlack, - StrokeWidth: 1, - FillColor: drawing.ColorBlack.WithAlpha(60), - DotWidth: 2, - DotColor: drawing.ColorBlack, - DotFillColor: drawing.ColorWhite, - } - d, err := NewDraw(DrawOption{ - Width: 400, - Height: 300, - }) - assert.Nil(err) - d.lineFill([]Point{ - { - X: 0, - Y: 0, - }, - { - X: 10, - Y: 20, - }, - { - X: 50, - Y: 60, - }, - }, ls) - data, err := d.Bytes() - assert.Nil(err) - assert.Equal("\\n", string(data)) -} - -func TestDrawLineDot(t *testing.T) { - assert := assert.New(t) - - ls := LineStyle{ - StrokeColor: drawing.ColorBlack, - StrokeWidth: 1, - FillColor: drawing.ColorBlack.WithAlpha(60), - DotWidth: 2, - DotColor: drawing.ColorBlack, - DotFillColor: drawing.ColorWhite, - } - d, err := NewDraw(DrawOption{ - Width: 400, - Height: 300, - }) - assert.Nil(err) - d.lineDot([]Point{ - { - X: 0, - Y: 0, - }, - { - X: 10, - Y: 20, - }, - { - X: 50, - Y: 60, - }, - }, ls) - data, err := d.Bytes() - assert.Nil(err) - assert.Equal("\\n", string(data)) -} - -func TestDrawLine(t *testing.T) { - assert := assert.New(t) - - ls := LineStyle{ - StrokeColor: drawing.ColorBlack, - StrokeWidth: 1, - FillColor: drawing.ColorBlack.WithAlpha(60), - DotWidth: 2, - DotColor: drawing.ColorBlack, - DotFillColor: drawing.ColorWhite, - } - d, err := NewDraw(DrawOption{ - Width: 400, - Height: 300, - }) - assert.Nil(err) - d.Line([]Point{ - { - X: 0, - Y: 0, - }, - { - X: 10, - Y: 20, - }, - { - X: 50, - Y: 60, - }, - }, ls) - data, err := d.Bytes() - assert.Nil(err) - assert.Equal("\\n", string(data)) -} diff --git a/mark_line.go b/mark_line.go deleted file mode 100644 index 464fe71..0000000 --- a/mark_line.go +++ /dev/null @@ -1,92 +0,0 @@ -// 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" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" -) - -func NewMarkLine(markLineTypes ...string) SeriesMarkLine { - data := make([]SeriesMarkData, len(markLineTypes)) - for index, t := range markLineTypes { - data[index] = SeriesMarkData{ - Type: t, - } - } - return SeriesMarkLine{ - Data: data, - } -} - -type markLineRenderOption struct { - Draw *Draw - FillColor drawing.Color - FontColor drawing.Color - StrokeColor drawing.Color - Font *truetype.Font - Series *Series - Range *Range -} - -func markLineRender(opt markLineRenderOption) { - d := opt.Draw - s := opt.Series - if len(s.MarkLine.Data) == 0 { - return - } - r := d.Render - summary := s.Summary() - for _, markLine := range s.MarkLine.Data { - // 由于mark line会修改style,因此每次重新设置 - chart.Style{ - FillColor: opt.FillColor, - FontColor: opt.FontColor, - FontSize: labelFontSize, - StrokeColor: opt.StrokeColor, - StrokeWidth: 1, - Font: opt.Font, - StrokeDashArray: []float64{ - 4, - 2, - }, - }.WriteToRenderer(r) - value := float64(0) - switch markLine.Type { - case SeriesMarkDataTypeMax: - value = summary.MaxValue - case SeriesMarkDataTypeMin: - value = summary.MinValue - default: - value = summary.AverageValue - } - y := opt.Range.getRestHeight(value) - width := d.Box.Width() - text := commafWithDigits(value) - textBox := r.MeasureText(text) - d.makeLine(0, y, width-2) - d.text(text, width, y+textBox.Height()>>1-2) - } - -} diff --git a/mark_line_test.go b/mark_line_test.go deleted file mode 100644 index abb3308..0000000 --- a/mark_line_test.go +++ /dev/null @@ -1,99 +0,0 @@ -// 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" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" -) - -func TestNewMarkLine(t *testing.T) { - assert := assert.New(t) - - markLine := NewMarkLine( - SeriesMarkDataTypeMax, - SeriesMarkDataTypeMin, - SeriesMarkDataTypeAverage, - ) - - assert.Equal(SeriesMarkLine{ - Data: []SeriesMarkData{ - { - Type: SeriesMarkDataTypeMax, - }, - { - Type: SeriesMarkDataTypeMin, - }, - { - Type: SeriesMarkDataTypeAverage, - }, - }, - }, markLine) -} - -func TestMarkLineRender(t *testing.T) { - assert := assert.New(t) - - d, err := NewDraw(DrawOption{ - Width: 400, - Height: 300, - }, PaddingOption(chart.Box{ - Left: 20, - Right: 20, - })) - assert.Nil(err) - f, _ := chart.GetDefaultFont() - - markLineRender(markLineRenderOption{ - Draw: d, - FillColor: drawing.ColorBlack, - FontColor: drawing.ColorBlack, - StrokeColor: drawing.ColorBlack, - Font: f, - Series: &Series{ - MarkLine: NewMarkLine( - SeriesMarkDataTypeMax, - SeriesMarkDataTypeMin, - SeriesMarkDataTypeAverage, - ), - Data: NewSeriesDataFromValues([]float64{ - 1, - 3, - 5, - 7, - 9, - }), - }, - Range: &Range{ - Min: 0, - Max: 10, - Size: 200, - }, - }) - data, err := d.Bytes() - assert.Nil(err) - assert.Equal("\\n915", string(data)) -} diff --git a/mark_point.go b/mark_point.go deleted file mode 100644 index 5fd34c4..0000000 --- a/mark_point.go +++ /dev/null @@ -1,89 +0,0 @@ -// 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" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" -) - -func NewMarkPoint(markPointTypes ...string) SeriesMarkPoint { - data := make([]SeriesMarkData, len(markPointTypes)) - for index, t := range markPointTypes { - data[index] = SeriesMarkData{ - Type: t, - } - } - return SeriesMarkPoint{ - Data: data, - } -} - -type markPointRenderOption struct { - Draw *Draw - FillColor drawing.Color - Font *truetype.Font - Series *Series - Points []Point -} - -func markPointRender(opt markPointRenderOption) { - d := opt.Draw - s := opt.Series - if len(s.MarkPoint.Data) == 0 { - return - } - points := opt.Points - summary := s.Summary() - symbolSize := s.MarkPoint.SymbolSize - if symbolSize == 0 { - symbolSize = 30 - } - r := d.Render - // 设置填充样式 - chart.Style{ - FillColor: opt.FillColor, - }.WriteToRenderer(r) - // 设置文本样式 - chart.Style{ - FontColor: NewTheme(ThemeDark).GetTextColor(), - FontSize: labelFontSize, - StrokeWidth: 1, - Font: opt.Font, - }.WriteTextOptionsToRenderer(r) - for _, markPointData := range s.MarkPoint.Data { - p := points[summary.MinIndex] - value := summary.MinValue - switch markPointData.Type { - case SeriesMarkDataTypeMax: - p = points[summary.MaxIndex] - value = summary.MaxValue - } - - d.pin(p.X, p.Y-symbolSize>>1, symbolSize) - text := commafWithDigits(value) - textBox := r.MeasureText(text) - d.text(text, p.X-textBox.Width()>>1, p.Y-symbolSize>>1-2) - } -} diff --git a/mark_point_test.go b/mark_point_test.go deleted file mode 100644 index 2cd8fdd..0000000 --- a/mark_point_test.go +++ /dev/null @@ -1,103 +0,0 @@ -// 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" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" -) - -func TestNewMarkPoint(t *testing.T) { - assert := assert.New(t) - - markPoint := NewMarkPoint( - SeriesMarkDataTypeMax, - SeriesMarkDataTypeMin, - SeriesMarkDataTypeAverage, - ) - - assert.Equal(SeriesMarkPoint{ - Data: []SeriesMarkData{ - { - Type: SeriesMarkDataTypeMax, - }, - { - Type: SeriesMarkDataTypeMin, - }, - { - Type: SeriesMarkDataTypeAverage, - }, - }, - }, markPoint) -} - -func TestMarkPointRender(t *testing.T) { - assert := assert.New(t) - - d, err := NewDraw(DrawOption{ - Width: 400, - Height: 300, - }, PaddingOption(chart.Box{ - Left: 20, - Right: 20, - })) - assert.Nil(err) - f, _ := chart.GetDefaultFont() - - markPointRender(markPointRenderOption{ - Draw: d, - FillColor: drawing.ColorBlack, - Font: f, - Series: &Series{ - MarkPoint: NewMarkPoint( - SeriesMarkDataTypeMax, - SeriesMarkDataTypeMin, - ), - Data: NewSeriesDataFromValues([]float64{ - 1, - 3, - 5, - }), - }, - Points: []Point{ - { - X: 1, - Y: 50, - }, - { - X: 100, - Y: 100, - }, - { - X: 200, - Y: 200, - }, - }, - }) - data, err := d.Bytes() - assert.Nil(err) - assert.Equal("\\n51", string(data)) -} diff --git a/painter.go b/painter.go index 639371e..d762e86 100644 --- a/painter.go +++ b/painter.go @@ -135,6 +135,8 @@ func NewPainter(opts PainterOptions, opt ...PainterOption) (*Painter, error) { if err != nil { return nil, err } + r.SetFont(font) + p := &Painter{ render: r, box: Box{ @@ -167,6 +169,9 @@ func (p *Painter) Child(opt ...PainterOption) *Painter { } func (p *Painter) SetStyle(style Style) { + if style.Font == nil { + style.Font = p.font + } p.previousStyle = p.style p.style = style style.WriteToRenderer(p.render) @@ -179,6 +184,9 @@ func (p *Painter) SetDrawingStyle(style Style) { } func (p *Painter) SetTextStyle(style Style) { + if style.Font == nil { + style.Font = p.font + } p.previousStyle = p.style p.style = style style.WriteTextOptionsToRenderer(p.render) diff --git a/painter_test.go b/painter_test.go index 425dbbe..1cc08be 100644 --- a/painter_test.go +++ b/painter_test.go @@ -94,7 +94,7 @@ func TestPainter(t *testing.T) { fn: func(p *Painter) { p.Text("hello world!", 3, 6) }, - result: "\\nhello world!", + result: "\\nhello world!", }, // line stroke { diff --git a/pie_chart.go b/pie_chart.go deleted file mode 100644 index 099a91c..0000000 --- a/pie_chart.go +++ /dev/null @@ -1,159 +0,0 @@ -// 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 ( - "errors" - "math" - - "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2" -) - -func getPieStyle(theme *Theme, index int) chart.Style { - seriesColor := theme.GetSeriesColor(index) - return chart.Style{ - StrokeColor: seriesColor, - StrokeWidth: 1, - FillColor: seriesColor, - } -} - -type pieChartOption struct { - Theme string - Font *truetype.Font - SeriesList SeriesList -} - -func pieChartRender(opt pieChartOption, result *basicRenderResult) error { - d, err := NewDraw(DrawOption{ - Parent: result.d, - }, PaddingOption(chart.Box{ - Top: result.titleBox.Height(), - })) - if err != nil { - return err - } - - values := make([]float64, len(opt.SeriesList)) - total := float64(0) - radiusValue := "" - for index, series := range opt.SeriesList { - if len(series.Radius) != 0 { - radiusValue = series.Radius - } - value := float64(0) - for _, item := range series.Data { - value += item.Value - } - values[index] = value - total += value - } - if total <= 0 { - return errors.New("The sum value of pie chart should gt 0") - } - r := d.Render - theme := NewTheme(opt.Theme) - - box := d.Box - cx := box.Width() >> 1 - cy := box.Height() >> 1 - - diameter := chart.MinInt(box.Width(), box.Height()) - radius := getRadius(float64(diameter), radiusValue) - - labelLineWidth := 15 - if radius < 50 { - labelLineWidth = 10 - } - labelRadius := radius + float64(labelLineWidth) - - seriesNames := opt.SeriesList.Names() - - if len(values) == 1 { - getPieStyle(theme, 0).WriteToRenderer(r) - d.moveTo(cx, cy) - d.circle(radius, cx, cy) - } else { - currentValue := float64(0) - for index, v := range values { - - pieStyle := getPieStyle(theme, index) - pieStyle.WriteToRenderer(r) - d.moveTo(cx, cy) - start := chart.PercentToRadians(currentValue/total) - math.Pi/2 - currentValue += v - percent := (v / total) - delta := chart.PercentToRadians(percent) - d.arcTo(cx, cy, radius, radius, start, delta) - d.lineTo(cx, cy) - r.Close() - r.FillStroke() - - series := opt.SeriesList[index] - // 是否显示label - showLabel := series.Label.Show - if !showLabel { - continue - } - - // label的角度为饼块中间 - angle := start + delta/2 - startx := cx + int(radius*math.Cos(angle)) - starty := cy + int(radius*math.Sin(angle)) - - endx := cx + int(labelRadius*math.Cos(angle)) - endy := cy + int(labelRadius*math.Sin(angle)) - d.moveTo(startx, starty) - d.lineTo(endx, endy) - offset := labelLineWidth - if endx < cx { - offset *= -1 - } - d.moveTo(endx, endy) - endx += offset - d.lineTo(endx, endy) - r.Stroke() - textStyle := chart.Style{ - FontColor: theme.GetTextColor(), - FontSize: labelFontSize, - Font: opt.Font, - } - if !series.Label.Color.IsZero() { - textStyle.FontColor = series.Label.Color - } - textStyle.GetTextOptions().WriteToRenderer(r) - text := NewPieLabelFormatter(seriesNames, series.Label.Formatter)(index, v, percent) - textBox := r.MeasureText(text) - textMargin := 3 - x := endx + textMargin - y := endy + textBox.Height()>>1 - 1 - if offset < 0 { - textWidth := textBox.Width() - x = endx - textWidth - textMargin - } - d.text(text, x, y) - } - } - return nil -} diff --git a/pie_chart_test.go b/pie_chart_test.go deleted file mode 100644 index 84072be..0000000 --- a/pie_chart_test.go +++ /dev/null @@ -1,69 +0,0 @@ -// 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" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" -) - -func TestPieChartRender(t *testing.T) { - assert := assert.New(t) - - d, err := NewDraw(DrawOption{ - Width: 250, - Height: 150, - }) - assert.Nil(err) - - f, _ := chart.GetDefaultFont() - - err = pieChartRender(pieChartOption{ - Font: f, - SeriesList: NewPieSeriesList([]float64{ - 5, - 10, - 0, - }, PieSeriesOption{ - Names: []string{ - "a", - "b", - "c", - }, - Label: SeriesLabel{ - Show: true, - Color: drawing.ColorRed, - }, - Radius: "20%", - }), - }, &basicRenderResult{ - d: d, - }) - assert.Nil(err) - data, err := d.Bytes() - assert.Nil(err) - assert.Equal("\\na: 33.33%b: 66.66%c: 0%", string(data)) -} diff --git a/radar_chart.go b/radar_chart.go deleted file mode 100644 index 364213d..0000000 --- a/radar_chart.go +++ /dev/null @@ -1,193 +0,0 @@ -// 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 ( - "errors" - - "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" -) - -type RadarIndicator struct { - // Indicator's name - Name string - // The maximum value of indicator - Max float64 - // The minimum value of indicator - Min float64 -} - -type radarChartOption struct { - Theme string - Font *truetype.Font - SeriesList SeriesList - Indicators []RadarIndicator -} - -func radarChartRender(opt radarChartOption, result *basicRenderResult) error { - sides := len(opt.Indicators) - if sides < 3 { - return errors.New("The count of indicator should be >= 3") - } - maxValues := make([]float64, len(opt.Indicators)) - for _, series := range opt.SeriesList { - for index, item := range series.Data { - if index < len(maxValues) && item.Value > maxValues[index] { - maxValues[index] = item.Value - } - } - } - for index, indicator := range opt.Indicators { - if indicator.Max <= 0 { - opt.Indicators[index].Max = maxValues[index] - } - } - d, err := NewDraw(DrawOption{ - Parent: result.d, - }, PaddingOption(chart.Box{ - Top: result.titleBox.Height(), - })) - if err != nil { - return err - } - radiusValue := "" - for _, series := range opt.SeriesList { - if len(series.Radius) != 0 { - radiusValue = series.Radius - } - } - - box := d.Box - cx := box.Width() >> 1 - cy := box.Height() >> 1 - diameter := chart.MinInt(box.Width(), box.Height()) - radius := getRadius(float64(diameter), radiusValue) - - theme := NewTheme(opt.Theme) - - divideCount := 5 - divideRadius := float64(int(radius / float64(divideCount))) - radius = divideRadius * float64(divideCount) - - style := chart.Style{ - StrokeColor: theme.GetAxisSplitLineColor(), - StrokeWidth: 1, - } - r := d.Render - style.WriteToRenderer(r) - center := Point{ - X: cx, - Y: cy, - } - for i := 0; i < divideCount; i++ { - d.polygon(center, divideRadius*float64(i+1), sides) - } - points := getPolygonPoints(center, radius, sides) - for _, p := range points { - d.moveTo(center.X, center.Y) - d.lineTo(p.X, p.Y) - d.Render.Stroke() - } - // 文本 - textStyle := chart.Style{ - FontColor: theme.GetTextColor(), - FontSize: labelFontSize, - Font: opt.Font, - } - textStyle.GetTextOptions().WriteToRenderer(r) - offset := 5 - // 文本生成 - for index, p := range points { - name := opt.Indicators[index].Name - b := r.MeasureText(name) - isXCenter := p.X == center.X - isYCenter := p.Y == center.Y - isRight := p.X > center.X - isLeft := p.X < center.X - isTop := p.Y < center.Y - isBottom := p.Y > center.Y - x := p.X - y := p.Y - if isXCenter { - x -= b.Width() >> 1 - if isTop { - y -= b.Height() - } else { - y += b.Height() - } - } - if isYCenter { - y += b.Height() >> 1 - } - if isTop { - y += offset - } - if isBottom { - y += offset - } - if isRight { - x += offset - } - if isLeft { - x -= (b.Width() + offset) - } - d.text(name, x, y) - } - - // 雷达图 - angles := getPolygonPointAngles(sides) - maxCount := len(opt.Indicators) - for _, series := range opt.SeriesList { - linePoints := make([]Point, 0, maxCount) - for j, item := range series.Data { - if j >= maxCount { - continue - } - indicator := opt.Indicators[j] - percent := (item.Value - indicator.Min) / (indicator.Max - indicator.Min) - r := percent * radius - p := getPolygonPoint(center, r, angles[j]) - linePoints = append(linePoints, p) - } - color := theme.GetSeriesColor(series.index) - dotFillColor := drawing.ColorWhite - if theme.IsDark() { - dotFillColor = color - } - linePoints = append(linePoints, linePoints[0]) - s := LineStyle{ - StrokeColor: color, - StrokeWidth: defaultStrokeWidth, - DotWidth: defaultDotWidth, - DotColor: color, - DotFillColor: dotFillColor, - FillColor: color.WithAlpha(20), - } - d.lineStroke(linePoints, s) - d.fill(linePoints, s.Style()) - d.lineDot(linePoints[0:len(linePoints)-1], s) - } - return nil -} diff --git a/radar_chart_test.go b/radar_chart_test.go deleted file mode 100644 index c5d2aa9..0000000 --- a/radar_chart_test.go +++ /dev/null @@ -1,102 +0,0 @@ -// 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" - "github.com/wcharczuk/go-chart/v2" -) - -func TestRadarChartRender(t *testing.T) { - assert := assert.New(t) - - d, err := NewDraw(DrawOption{ - Width: 250, - Height: 150, - }) - assert.Nil(err) - - f, _ := chart.GetDefaultFont() - err = radarChartRender(radarChartOption{ - Font: f, - Indicators: []RadarIndicator{ - { - Name: "Sales", - Max: 6500, - }, - { - Name: "Administration", - Max: 16000, - }, - { - Name: "Information Technology", - Max: 30000, - }, - { - Name: "Customer Support", - Max: 38000, - }, - { - Name: "Development", - Max: 52000, - }, - { - Name: "Marketing", - Max: 25000, - }, - }, - SeriesList: SeriesList{ - { - Type: ChartTypeRadar, - Data: NewSeriesDataFromValues([]float64{ - 4200, - 3000, - 20000, - 35000, - 50000, - 18000, - }), - }, - { - Type: ChartTypeRadar, - index: 1, - Data: NewSeriesDataFromValues([]float64{ - 5000, - 14000, - 28000, - 26000, - 42000, - 21000, - }), - }, - }, - }, &basicRenderResult{ - d: d, - }) - assert.Nil(err) - data, err := d.Bytes() - assert.Nil(err) - assert.Equal("\\nSalesAdministrationInformation TechnologyCustomer SupportDevelopmentMarketing", string(data)) -} diff --git a/range.go b/range.go deleted file mode 100644 index 255a51b..0000000 --- a/range.go +++ /dev/null @@ -1,109 +0,0 @@ -// 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 := 2 - if r > 10 { - unit = 4 - } - if r > 30 { - unit = 5 - } - if r > 100 { - unit = 10 - } - if r > 200 { - unit = 20 - } - unit = int((r/float64(divideCount))/float64(unit))*unit + unit - - if min != 0 { - isLessThanZero := min < 0 - min = float64(int(min/float64(unit)) * unit) - // 如果是小于0,int的时候向上取整了,因此调整 - if min < 0 || - (isLessThanZero && min == 0) { - min -= float64(unit) - } - } - max = min + float64(unit*divideCount) - return Range{ - Min: min, - Max: max, - divideCount: divideCount, - } -} - -func (r Range) Values() []string { - offset := (r.Max - r.Min) / float64(r.divideCount) - values := make([]string, 0) - for i := 0; i <= r.divideCount; i++ { - v := r.Min + float64(i)*offset - value := commafWithDigits(v) - values = append(values, value) - } - return values -} - -func (r *Range) getHeight(value float64) int { - v := (value - r.Min) / (r.Max - r.Min) - return int(v * float64(r.Size)) -} - -func (r *Range) getRestHeight(value float64) int { - return r.Size - r.getHeight(value) -} - -func (r *Range) GetRange(index int) (float64, float64) { - unit := float64(r.Size) / float64(r.divideCount) - return unit * float64(index), unit * float64(index+1) -} -func (r *Range) AutoDivide() []int { - return autoDivide(r.Size, r.divideCount) -} - -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)) -} diff --git a/range_test.go b/range_test.go deleted file mode 100644 index d1aea8f..0000000 --- a/range_test.go +++ /dev/null @@ -1,94 +0,0 @@ -// 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(12.0, r.Max) - - r = NewRange(0, 12, 6) - assert.Equal(0.0, r.Min) - assert.Equal(24.0, r.Max) - - r = NewRange(-13, 18, 6) - assert.Equal(-20.0, r.Min) - assert.Equal(40.0, r.Max) - - r = NewRange(0, 150, 6) - assert.Equal(0.0, r.Min) - assert.Equal(180.0, r.Max) - - r = NewRange(0, 400, 6) - assert.Equal(0.0, r.Min) - assert.Equal(480.0, r.Max) -} - -func TestRangeHeightWidth(t *testing.T) { - assert := assert.New(t) - r := NewRange(0, 8, 6) - r.Size = 100 - - assert.Equal(33, r.getHeight(4)) - assert.Equal(67, r.getRestHeight(4)) - - assert.Equal(33, r.getWidth(4)) - r.Boundary = true - assert.Equal(41, r.getWidth(4)) -} - -func TestRangeGetRange(t *testing.T) { - assert := assert.New(t) - r := NewRange(0, 8, 6) - r.Size = 120 - - f1, f2 := r.GetRange(0) - assert.Equal(0.0, f1) - assert.Equal(20.0, f2) - - f1, f2 = r.GetRange(2) - assert.Equal(40.0, f1) - assert.Equal(60.0, f2) -} - -func TestRangeAutoDivide(t *testing.T) { - assert := assert.New(t) - - r := Range{ - Size: 120, - divideCount: 6, - } - - assert.Equal([]int{0, 20, 40, 60, 80, 100, 120}, r.AutoDivide()) - - r.Size = 130 - assert.Equal([]int{0, 22, 44, 66, 88, 109, 130}, r.AutoDivide()) -} diff --git a/series.go b/series.go deleted file mode 100644 index 14227d1..0000000 --- a/series.go +++ /dev/null @@ -1,233 +0,0 @@ -// 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" - "strings" - - "github.com/dustin/go-humanize" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" -) - -type SeriesData struct { - // The value of series data - Value float64 - // The style of series data - Style chart.Style -} - -func NewSeriesFromValues(values []float64, chartType ...string) Series { - s := Series{ - Data: NewSeriesDataFromValues(values), - } - if len(chartType) != 0 { - s.Type = chartType[0] - } - return s -} - -func NewSeriesDataFromValues(values []float64) []SeriesData { - data := make([]SeriesData, len(values)) - for index, value := range values { - data[index] = SeriesData{ - Value: value, - } - } - return data -} - -type SeriesLabel struct { - // Data label formatter, which supports string template. - // {b}: the name of a data item. - // {c}: the value of a data item. - // {d}: the percent of a data item(pie chart). - Formatter string - // The color for label - Color drawing.Color - // Show flag for label - Show bool - // Distance to the host graphic element. - Distance int -} - -const ( - SeriesMarkDataTypeMax = "max" - SeriesMarkDataTypeMin = "min" - SeriesMarkDataTypeAverage = "average" -) - -type SeriesMarkData struct { - // The mark data type, it can be "max", "min", "average". - // The "average" is only for mark line - Type string -} -type SeriesMarkPoint struct { - // The width of symbol, default value is 30 - SymbolSize int - // The mark data of series mark point - Data []SeriesMarkData -} -type SeriesMarkLine struct { - // The mark data of series mark line - Data []SeriesMarkData -} -type Series struct { - index int - // The type of series, it can be "line", "bar" or "pie". - // Default value is "line" - Type string - // The data list of series - Data []SeriesData - // The Y axis index, it should be 0 or 1. - // Default value is 1 - YAxisIndex int - // The style for series - Style chart.Style - // The label for series - Label SeriesLabel - // The name of series - Name string - // Radius for Pie chart, e.g.: 40%, default is "40%" - Radius string - // Mark point for series - MarkPoint SeriesMarkPoint - // Make line for series - MarkLine SeriesMarkLine - // Max value of series - Min *float64 - // Min value of series - Max *float64 -} -type SeriesList []Series - -type PieSeriesOption struct { - Radius string - Label SeriesLabel - Names []string -} - -func NewPieSeriesList(values []float64, opts ...PieSeriesOption) SeriesList { - result := make([]Series, len(values)) - var opt PieSeriesOption - if len(opts) != 0 { - opt = opts[0] - } - for index, v := range values { - name := "" - if index < len(opt.Names) { - name = opt.Names[index] - } - s := Series{ - Type: ChartTypePie, - Data: []SeriesData{ - { - Value: v, - }, - }, - Radius: opt.Radius, - Label: opt.Label, - Name: name, - } - result[index] = s - } - return result -} - -type seriesSummary struct { - MaxIndex int - MaxValue float64 - MinIndex int - MinValue float64 - AverageValue float64 -} - -func (s *Series) Summary() seriesSummary { - minIndex := -1 - maxIndex := -1 - minValue := math.MaxFloat64 - maxValue := -math.MaxFloat64 - sum := float64(0) - for j, item := range s.Data { - if item.Value < minValue { - minIndex = j - minValue = item.Value - } - if item.Value > maxValue { - maxIndex = j - maxValue = item.Value - } - sum += item.Value - } - return seriesSummary{ - MaxIndex: maxIndex, - MaxValue: maxValue, - MinIndex: minIndex, - MinValue: minValue, - AverageValue: sum / float64(len(s.Data)), - } -} - -func (sl SeriesList) Names() []string { - names := make([]string, len(sl)) - for index, s := range sl { - names[index] = s.Name - } - return names -} - -type LabelFormatter func(index int, value float64, percent float64) string - -func NewPieLabelFormatter(seriesNames []string, layout string) LabelFormatter { - if len(layout) == 0 { - layout = "{b}: {d}" - } - return NewLabelFormatter(seriesNames, layout) -} - -func NewValueLabelFormater(seriesNames []string, layout string) LabelFormatter { - if len(layout) == 0 { - layout = "{c}" - } - return NewLabelFormatter(seriesNames, layout) -} - -func NewLabelFormatter(seriesNames []string, layout string) LabelFormatter { - return func(index int, value, percent float64) string { - // 如果无percent的则设置为<0 - percentText := "" - if percent >= 0 { - percentText = humanize.FtoaWithDigits(percent*100, 2) + "%" - } - valueText := humanize.FtoaWithDigits(value, 2) - name := "" - if len(seriesNames) > index { - name = seriesNames[index] - } - text := strings.ReplaceAll(layout, "{c}", valueText) - text = strings.ReplaceAll(text, "{d}", percentText) - text = strings.ReplaceAll(text, "{b}", name) - return text - } -} diff --git a/series_test.go b/series_test.go deleted file mode 100644 index 1460180..0000000 --- a/series_test.go +++ /dev/null @@ -1,166 +0,0 @@ -// 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 TestNewSeriesFromValues(t *testing.T) { - assert := assert.New(t) - - assert.Equal(Series{ - Data: []SeriesData{ - { - Value: 1, - }, - { - Value: 2, - }, - }, - Type: ChartTypeBar, - }, NewSeriesFromValues([]float64{ - 1, - 2, - }, ChartTypeBar)) -} - -func TestNewSeriesDataFromValues(t *testing.T) { - assert := assert.New(t) - - assert.Equal([]SeriesData{ - { - Value: 1, - }, - { - Value: 2, - }, - }, NewSeriesDataFromValues([]float64{ - 1, - 2, - })) -} - -func TestNewPieSeriesList(t *testing.T) { - assert := assert.New(t) - - assert.Equal(SeriesList{ - { - Type: ChartTypePie, - Name: "a", - Label: SeriesLabel{ - Show: true, - }, - Radius: "30%", - Data: []SeriesData{ - { - Value: 1, - }, - }, - }, - { - Type: ChartTypePie, - Name: "b", - Label: SeriesLabel{ - Show: true, - }, - Radius: "30%", - Data: []SeriesData{ - { - Value: 2, - }, - }, - }, - }, NewPieSeriesList([]float64{ - 1, - 2, - }, PieSeriesOption{ - Radius: "30%", - Label: SeriesLabel{ - Show: true, - }, - Names: []string{ - "a", - "b", - }, - })) -} - -func TestSeriesSummary(t *testing.T) { - assert := assert.New(t) - - s := Series{ - Data: NewSeriesDataFromValues([]float64{ - 1, - 3, - 5, - 7, - 9, - }), - } - assert.Equal(seriesSummary{ - MaxIndex: 4, - MaxValue: 9, - MinIndex: 0, - MinValue: 1, - AverageValue: 5, - }, s.Summary()) -} - -func TestGetSeriesNames(t *testing.T) { - assert := assert.New(t) - - sl := SeriesList{ - { - Name: "a", - }, - { - Name: "b", - }, - } - assert.Equal([]string{ - "a", - "b", - }, sl.Names()) -} - -func TestNewPieLabelFormatter(t *testing.T) { - assert := assert.New(t) - - fn := NewPieLabelFormatter([]string{ - "a", - "b", - }, "") - assert.Equal("a: 35%", fn(0, 1.2, 0.35)) -} - -func TestNewValueLabelFormater(t *testing.T) { - assert := assert.New(t) - fn := NewValueLabelFormater([]string{ - "a", - "b", - }, "") - assert.Equal("1.2", fn(0, 1.2, 0.35)) -} diff --git a/table.go b/table.go deleted file mode 100644 index 9cfc6b1..0000000 --- a/table.go +++ /dev/null @@ -1,145 +0,0 @@ -// 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 ( - "errors" - - "github.com/wcharczuk/go-chart/v2" -) - -type TableOption struct { - // draw - Draw *Draw - // The width of table - Width int - // The header of table - Header []string - // The style of table - Style chart.Style - ColumnWidths []float64 - // 是否仅测量高度 - measurement bool -} - -var ErrTableColumnWidthInvalid = errors.New("table column width is invalid") - -func tableDivideWidth(width, size int, columnWidths []float64) ([]int, error) { - widths := make([]int, size) - - autoFillCount := size - restWidth := width - if len(columnWidths) != 0 { - for index, v := range columnWidths { - if v <= 0 { - continue - } - autoFillCount-- - // 小于1的表示占比 - if v < 1 { - widths[index] = int(v * float64(width)) - } else { - widths[index] = int(v) - } - restWidth -= widths[index] - } - } - if restWidth < 0 { - return nil, ErrTableColumnWidthInvalid - } - // 填充其它未指定的宽度 - if autoFillCount > 0 { - autoWidth := restWidth / autoFillCount - for index, v := range widths { - if v == 0 { - widths[index] = autoWidth - } - } - } - widthSum := 0 - for _, v := range widths { - widthSum += v - } - if widthSum > width { - return nil, ErrTableColumnWidthInvalid - } - return widths, nil -} - -func TableMeasure(opt TableOption) (chart.Box, error) { - d, err := NewDraw(DrawOption{ - Width: opt.Width, - Height: 600, - }) - if err != nil { - return chart.BoxZero, err - } - opt.Draw = d - opt.measurement = true - return tableRender(opt) -} - -func tableRender(opt TableOption) (chart.Box, error) { - if opt.Draw == nil { - return chart.BoxZero, errors.New("draw can not be nil") - } - if len(opt.Header) == 0 { - return chart.BoxZero, errors.New("header can not be nil") - } - width := opt.Width - if width == 0 { - width = opt.Draw.Box.Width() - } - - columnWidths, err := tableDivideWidth(width, len(opt.Header), opt.ColumnWidths) - if err != nil { - return chart.BoxZero, err - } - - d := opt.Draw - style := opt.Style - y := 0 - x := 0 - - headerMaxHeight := 0 - for index, text := range opt.Header { - var box chart.Box - w := columnWidths[index] - y0 := y + int(opt.Style.FontSize) - if opt.measurement { - box = d.measureTextFit(text, x, y0, w, style) - } else { - box = d.textFit(text, x, y0, w, style) - } - if box.Height() > headerMaxHeight { - headerMaxHeight = box.Height() - } - x += w - } - y += headerMaxHeight - - return chart.Box{ - Right: width, - Bottom: y, - }, nil -} diff --git a/theme.go b/theme.go index e3f9773..88c73df 100644 --- a/theme.go +++ b/theme.go @@ -22,10 +22,6 @@ package charts -import ( - "github.com/wcharczuk/go-chart/v2/drawing" -) - const ThemeDark = "dark" const ThemeLight = "light" const ThemeGrafana = "grafana" @@ -37,198 +33,9 @@ type Theme struct { type themeColorPalette struct { isDarkMode bool - axisStrokeColor drawing.Color - axisSplitLineColor drawing.Color - backgroundColor drawing.Color - textColor drawing.Color - seriesColors []drawing.Color -} - -var palettes = map[string]*themeColorPalette{} - -func init() { - echartSeriesColors := []drawing.Color{ - parseColor("#5470c6"), - parseColor("#91cc75"), - parseColor("#fac858"), - parseColor("#ee6666"), - parseColor("#73c0de"), - parseColor("#3ba272"), - parseColor("#fc8452"), - parseColor("#9a60b4"), - parseColor("#ea7ccc"), - } - grafanaSeriesColors := []drawing.Color{ - parseColor("#7EB26D"), - parseColor("#EAB839"), - parseColor("#6ED0E0"), - parseColor("#EF843C"), - parseColor("#E24D42"), - parseColor("#1F78C1"), - parseColor("#705DA0"), - parseColor("#508642"), - } - antSeriesColors := []drawing.Color{ - parseColor("#5b8ff9"), - parseColor("#5ad8a6"), - parseColor("#5d7092"), - parseColor("#f6bd16"), - parseColor("#6f5ef9"), - parseColor("#6dc8ec"), - parseColor("#945fb9"), - parseColor("#ff9845"), - } - AddTheme( - ThemeDark, - true, - drawing.Color{ - R: 185, - G: 184, - B: 206, - A: 255, - }, - drawing.Color{ - R: 72, - G: 71, - B: 83, - A: 255, - }, - drawing.Color{ - R: 16, - G: 12, - B: 42, - A: 255, - }, - drawing.Color{ - R: 238, - G: 238, - B: 238, - A: 255, - }, - echartSeriesColors, - ) - - AddTheme( - ThemeLight, - false, - drawing.Color{ - R: 110, - G: 112, - B: 121, - A: 255, - }, - drawing.Color{ - R: 224, - G: 230, - B: 242, - A: 255, - }, - drawing.ColorWhite, - drawing.Color{ - R: 70, - G: 70, - B: 70, - A: 255, - }, - echartSeriesColors, - ) - AddTheme( - ThemeAnt, - false, - drawing.Color{ - R: 110, - G: 112, - B: 121, - A: 255, - }, - drawing.Color{ - R: 224, - G: 230, - B: 242, - A: 255, - }, - drawing.ColorWhite, - drawing.Color{ - R: 70, - G: 70, - B: 70, - A: 255, - }, - antSeriesColors, - ) - AddTheme( - ThemeGrafana, - true, - drawing.Color{ - R: 185, - G: 184, - B: 206, - A: 255, - }, - drawing.Color{ - R: 68, - G: 67, - B: 67, - A: 255, - }, - drawing.Color{ - R: 31, - G: 29, - B: 29, - A: 255, - }, - drawing.Color{ - R: 216, - G: 217, - B: 218, - A: 255, - }, - grafanaSeriesColors, - ) -} - -func AddTheme(name string, isDarkMode bool, axisStrokeColor, axisSplitLineColor, backgroundColor, textColor drawing.Color, seriesColors []drawing.Color) { - palettes[name] = &themeColorPalette{ - isDarkMode: isDarkMode, - axisStrokeColor: axisStrokeColor, - axisSplitLineColor: axisSplitLineColor, - backgroundColor: backgroundColor, - textColor: textColor, - seriesColors: seriesColors, - } -} - -func NewTheme(name string) *Theme { - p, ok := palettes[name] - if !ok { - p = palettes[ThemeLight] - } - return &Theme{ - palette: p, - } -} - -func (t *Theme) IsDark() bool { - return t.palette.isDarkMode -} - -func (t *Theme) GetAxisStrokeColor() drawing.Color { - return t.palette.axisStrokeColor -} - -func (t *Theme) GetAxisSplitLineColor() drawing.Color { - return t.palette.axisSplitLineColor -} - -func (t *Theme) GetSeriesColor(index int) drawing.Color { - colors := t.palette.seriesColors - return colors[index%len(colors)] -} - -func (t *Theme) GetBackgroundColor() drawing.Color { - return t.palette.backgroundColor -} - -func (t *Theme) GetTextColor() drawing.Color { - return t.palette.textColor + axisStrokeColor Color + axisSplitLineColor Color + backgroundColor Color + textColor Color + seriesColors []Color } diff --git a/theme_test.go b/theme_test.go deleted file mode 100644 index bf22afd..0000000 --- a/theme_test.go +++ /dev/null @@ -1,87 +0,0 @@ -// 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" - "github.com/wcharczuk/go-chart/v2/drawing" -) - -func TestTheme(t *testing.T) { - assert := assert.New(t) - - darkTheme := NewTheme(ThemeDark) - lightTheme := NewTheme(ThemeLight) - - assert.True(darkTheme.IsDark()) - assert.False(lightTheme.IsDark()) - - assert.Equal(drawing.Color{ - R: 185, - G: 184, - B: 206, - A: 255, - }, darkTheme.GetAxisStrokeColor()) - assert.Equal(drawing.Color{ - R: 110, - G: 112, - B: 121, - A: 255, - }, lightTheme.GetAxisStrokeColor()) - - assert.Equal(drawing.Color{ - R: 72, - G: 71, - B: 83, - A: 255, - }, darkTheme.GetAxisSplitLineColor()) - assert.Equal(drawing.Color{ - R: 224, - G: 230, - B: 242, - A: 255, - }, lightTheme.GetAxisSplitLineColor()) - - assert.Equal(drawing.Color{ - R: 16, - G: 12, - B: 42, - A: 255, - }, darkTheme.GetBackgroundColor()) - assert.Equal(drawing.ColorWhite, lightTheme.GetBackgroundColor()) - - assert.Equal(drawing.Color{ - R: 238, - G: 238, - B: 238, - A: 255, - }, darkTheme.GetTextColor()) - assert.Equal(drawing.Color{ - R: 70, - G: 70, - B: 70, - A: 255, - }, lightTheme.GetTextColor()) -} diff --git a/title.go b/title.go deleted file mode 100644 index 07a2eef..0000000 --- a/title.go +++ /dev/null @@ -1,155 +0,0 @@ -// 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 { - // Title text, support \n for new line - Text string - // Subtitle text, support \n for new line - Subtext string - // Title style - Style chart.Style - // Subtitle style - SubtextStyle chart.Style - // Distance between title component and the left side of the container. - // It can be pixel value: 20, percentage value: 20%, - // or position value: right, center. - Left string - // Distance between title component and the top side of the container. - // It can be pixel value: 20. - Top string -} -type titleMeasureOption struct { - width int - height int - text string - style chart.Style -} - -func splitTitleText(text string) []string { - arr := strings.Split(text, "\n") - result := make([]string, 0) - for _, v := range arr { - v = strings.TrimSpace(v) - if v == "" { - continue - } - result = append(result, v) - } - return result -} - -func drawTitle(p *Draw, opt *TitleOption) (chart.Box, error) { - if len(opt.Text) == 0 { - return chart.BoxZero, nil - } - - padding := opt.Style.Padding - d, err := NewDraw(DrawOption{ - Parent: p, - }, PaddingOption(padding)) - if err != nil { - return chart.BoxZero, err - } - - r := d.Render - - measureOptions := make([]titleMeasureOption, 0) - - // 主标题 - for _, v := range splitTitleText(opt.Text) { - measureOptions = append(measureOptions, titleMeasureOption{ - text: v, - style: opt.Style.GetTextOptions(), - }) - } - // 副标题 - for _, v := range splitTitleText(opt.Subtext) { - measureOptions = append(measureOptions, titleMeasureOption{ - text: v, - style: opt.SubtextStyle.GetTextOptions(), - }) - } - - textMaxWidth := 0 - textMaxHeight := 0 - width := 0 - for index, item := range measureOptions { - item.style.WriteTextOptionsToRenderer(r) - textBox := r.MeasureText(item.text) - - w := textBox.Width() - h := textBox.Height() - if w > textMaxWidth { - textMaxWidth = w - } - if h > textMaxHeight { - textMaxHeight = h - } - measureOptions[index].height = h - measureOptions[index].width = w - } - width = textMaxWidth - titleX := 0 - b := d.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 { - item.style.WriteTextOptionsToRenderer(r) - x := titleX + (textMaxWidth-item.width)>>1 - y := titleY + item.height - d.text(item.text, x, y) - titleY += item.height - } - height := titleY + padding.Top + padding.Bottom - box := padding.Clone() - box.Right = box.Left + titleX + width - box.Bottom = box.Top + height - - return box, nil -} diff --git a/title_test.go b/title_test.go deleted file mode 100644 index 23573c3..0000000 --- a/title_test.go +++ /dev/null @@ -1,142 +0,0 @@ -// 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" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" -) - -func TestSplitTitleText(t *testing.T) { - assert := assert.New(t) - - assert.Equal([]string{ - "a", - "b", - }, splitTitleText("a\nb")) - assert.Equal([]string{ - "a", - }, splitTitleText("a\n ")) -} - -func TestDrawTitle(t *testing.T) { - assert := assert.New(t) - - newOption := func() *TitleOption { - f, _ := chart.GetDefaultFont() - return &TitleOption{ - Text: "title\nHello", - Subtext: "subtitle\nWorld!", - Style: chart.Style{ - FontSize: 14, - Font: f, - FontColor: drawing.ColorBlack, - }, - SubtextStyle: chart.Style{ - FontSize: 10, - Font: f, - FontColor: drawing.ColorBlue, - }, - } - } - newDraw := func() *Draw { - d, _ := NewDraw(DrawOption{ - Width: 400, - Height: 300, - }) - return d - } - - tests := []struct { - newDraw func() *Draw - newOption func() *TitleOption - result string - box chart.Box - }{ - { - newDraw: newDraw, - newOption: newOption, - result: "\\ntitleHellosubtitleWorld!", - box: chart.Box{ - Right: 43, - Bottom: 58, - }, - }, - { - newDraw: newDraw, - newOption: func() *TitleOption { - opt := newOption() - opt.Left = PositionRight - opt.Top = "50" - return opt - }, - result: "\\ntitleHellosubtitleWorld!", - box: chart.Box{ - Right: 400, - Bottom: 108, - }, - }, - { - newDraw: newDraw, - newOption: func() *TitleOption { - opt := newOption() - opt.Left = PositionCenter - opt.Top = "10" - return opt - }, - result: "\\ntitleHellosubtitleWorld!", - box: chart.Box{ - Right: 222, - Bottom: 68, - }, - }, - { - newDraw: newDraw, - newOption: func() *TitleOption { - opt := newOption() - opt.Left = "10%" - opt.Top = "10" - return opt - }, - result: "\\ntitleHellosubtitleWorld!", - box: chart.Box{ - Right: 83, - Bottom: 68, - }, - }, - } - for _, tt := range tests { - d := tt.newDraw() - o := tt.newOption() - b, err := drawTitle(d, o) - assert.Nil(err) - assert.Equal(tt.box, b) - data, err := d.Bytes() - assert.Nil(err) - assert.NotEmpty(data) - assert.Equal(tt.result, string(data)) - } -} diff --git a/util.go b/util.go index d35b4b0..5fee163 100644 --- a/util.go +++ b/util.go @@ -134,8 +134,8 @@ func commafWithDigits(value float64) string { return humanize.CommafWithDigits(value, decimals) } -func parseColor(color string) drawing.Color { - c := drawing.Color{} +func parseColor(color string) Color { + c := Color{} if color == "" { return c } diff --git a/util_test.go b/util_test.go index 6489ab3..fefbabc 100644 --- a/util_test.go +++ b/util_test.go @@ -80,13 +80,15 @@ func TestGetRadius(t *testing.T) { func TestMeasureTextMaxWidthHeight(t *testing.T) { assert := assert.New(t) - r, err := chart.SVG(400, 300) + p, err := NewPainter(PainterOptions{ + Width: 400, + Height: 300, + }) assert.Nil(err) style := chart.Style{ FontSize: 10, } - style.Font, _ = chart.GetDefaultFont() - style.WriteToRenderer(r) + p.SetStyle(style) maxWidth, maxHeight := measureTextMaxWidthHeight([]string{ "Mon", @@ -96,7 +98,7 @@ func TestMeasureTextMaxWidthHeight(t *testing.T) { "Fri", "Sat", "Sun", - }, r) + }, p) assert.Equal(26, maxWidth) assert.Equal(12, maxHeight) } diff --git a/xaxis.go b/xaxis.go deleted file mode 100644 index d79f40e..0000000 --- a/xaxis.go +++ /dev/null @@ -1,93 +0,0 @@ -// 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 XAxisOption struct { - Font *truetype.Font - // The boundary gap on both sides of a coordinate axis. - // Nil or *true means the center part of two axis ticks - BoundaryGap *bool - // The data value of x axis - Data []string - // The theme of chart - Theme string - // Hidden x axis - Hidden bool - // Number of segments that the axis is split into. Note that this number serves only as a recommendation. - SplitNumber int -} - -func NewXAxisOption(data []string, boundaryGap ...*bool) XAxisOption { - opt := XAxisOption{ - Data: data, - } - if len(boundaryGap) != 0 { - opt.BoundaryGap = boundaryGap[0] - } - return opt -} - -// drawXAxis draws x axis, and returns the height, range of if. -func drawXAxis(p *Painter, opt *XAxisOption, yAxisCount int) (int, *Range, error) { - if opt.Hidden { - return 0, nil, nil - } - left := YAxisWidth - right := (yAxisCount - 1) * YAxisWidth - pXAxis := p.Child( - PainterPaddingOption(Box{ - Left: left, - Right: right, - }), - PainterFontOption(opt.Font), - ) - theme := NewTheme(opt.Theme) - data := NewAxisDataListFromStringList(opt.Data) - style := AxisOption{ - BoundaryGap: opt.BoundaryGap, - StrokeColor: theme.GetAxisStrokeColor(), - FontColor: theme.GetAxisStrokeColor(), - StrokeWidth: 1, - SplitNumber: opt.SplitNumber, - } - - boundary := true - max := float64(len(opt.Data)) - if isFalse(opt.BoundaryGap) { - boundary = false - max-- - } - axis := NewAxis(pXAxis, data, style) - axis.Render() - return axis.measure().Height, &Range{ - divideCount: len(opt.Data), - Min: 0, - Max: max, - Size: pXAxis.Width(), - Boundary: boundary, - }, nil -} diff --git a/xaxis_test.go b/xaxis_test.go deleted file mode 100644 index 267cdb1..0000000 --- a/xaxis_test.go +++ /dev/null @@ -1,108 +0,0 @@ -// 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 TestNewXAxisOption(t *testing.T) { - assert := assert.New(t) - - opt := NewXAxisOption([]string{ - "a", - "b", - }, FalseFlag()) - - assert.Equal(XAxisOption{ - Data: []string{ - "a", - "b", - }, - BoundaryGap: FalseFlag(), - }, opt) - -} -func TestDrawXAxis(t *testing.T) { - assert := assert.New(t) - - newDraw := func() *Draw { - d, _ := NewDraw(DrawOption{ - Width: 400, - Height: 300, - }) - return d - } - - tests := []struct { - newDraw func() *Draw - newOption func() *XAxisOption - result string - }{ - { - newDraw: newDraw, - newOption: func() *XAxisOption { - return &XAxisOption{ - BoundaryGap: FalseFlag(), - Data: []string{ - "Mon", - "Tue", - }, - } - }, - result: "\\nMonTue", - }, - { - newDraw: newDraw, - newOption: func() *XAxisOption { - return &XAxisOption{ - Data: []string{ - "01-01", - "01-02", - "01-03", - "01-04", - "01-05", - "01-06", - "01-07", - "01-08", - "01-09", - }, - SplitNumber: 3, - } - }, - result: "\\n01-0201-0501-08", - }, - } - - for _, tt := range tests { - d := tt.newDraw() - height, _, err := drawXAxis(d, tt.newOption(), 1) - assert.Nil(err) - assert.Equal(25, height) - data, err := d.Bytes() - assert.Nil(err) - assert.Equal(tt.result, string(data)) - } -} diff --git a/yaxis.go b/yaxis.go deleted file mode 100644 index 5d55440..0000000 --- a/yaxis.go +++ /dev/null @@ -1,95 +0,0 @@ -// 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 ( - "strings" - - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" -) - -type YAxisOption struct { - // The minimun value of axis. - Min *float64 - // The maximum value of axis. - Max *float64 - // Hidden y axis - Hidden bool - // Formatter for y axis text value - Formatter string - // Color for y axis - Color drawing.Color -} - -// TODO 长度是否可以变化 -const YAxisWidth = 40 - -func drawYAxis(p *Painter, opt *ChartOption, axisIndex, xAxisHeight int, padding chart.Box) (*Range, error) { - theme := NewTheme(opt.Theme) - yRange := opt.newYRange(axisIndex) - values := yRange.Values() - yAxis := opt.YAxisList[axisIndex] - formatter := yAxis.Formatter - if len(formatter) != 0 { - for index, text := range values { - values[index] = strings.ReplaceAll(formatter, "{value}", text) - } - } - - data := NewAxisDataListFromStringList(values) - style := AxisOption{ - Position: PositionLeft, - BoundaryGap: FalseFlag(), - FontColor: theme.GetAxisStrokeColor(), - TickShow: FalseFlag(), - StrokeWidth: 1, - SplitLineColor: theme.GetAxisSplitLineColor(), - SplitLineShow: true, - } - if !yAxis.Color.IsZero() { - style.FontColor = yAxis.Color - style.StrokeColor = yAxis.Color - } - width := NewAxis(p, data, style).measure().Width - - yAxisCount := len(opt.YAxisList) - boxWidth := p.Width() - if axisIndex > 0 { - style.SplitLineShow = false - style.Position = PositionRight - padding.Right += (axisIndex - 1) * YAxisWidth - } else { - boxWidth = p.Width() - (yAxisCount-1)*YAxisWidth - padding.Left += (YAxisWidth - width) - } - - pYAxis := p.Child( - PainterWidthHeightOption(boxWidth, p.Height()-xAxisHeight), - PainterPaddingOption(padding), - PainterFontOption(opt.Font), - ) - NewAxis(pYAxis, data, style).Render() - yRange.Size = pYAxis.Height() - return &yRange, nil -} diff --git a/yaxis_test.go b/yaxis_test.go deleted file mode 100644 index 0bbef7a..0000000 --- a/yaxis_test.go +++ /dev/null @@ -1,119 +0,0 @@ -// 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" - "github.com/wcharczuk/go-chart/v2" -) - -func TestDrawYAxis(t *testing.T) { - assert := assert.New(t) - newDraw := func() *Draw { - d, _ := NewDraw(DrawOption{ - Width: 400, - Height: 300, - }) - return d - } - - tests := []struct { - newDraw func() *Draw - newOption func() *ChartOption - axisIndex int - xAxisHeight int - result string - }{ - { - newDraw: newDraw, - newOption: func() *ChartOption { - return &ChartOption{ - YAxisList: []YAxisOption{ - { - Max: NewFloatPoint(20), - }, - }, - SeriesList: []Series{ - { - Data: []SeriesData{ - { - Value: 1, - }, - { - Value: 2, - }, - }, - }, - }, - } - }, - result: "\\n03.336.661013.3316.6620", - }, - { - newDraw: newDraw, - newOption: func() *ChartOption { - return &ChartOption{ - YAxisList: []YAxisOption{ - {}, - { - Max: NewFloatPoint(20), - Formatter: "{value} C", - }, - }, - SeriesList: []Series{ - { - YAxisIndex: 1, - Data: []SeriesData{ - { - Value: 1, - }, - { - Value: 2, - }, - }, - }, - }, - } - }, - axisIndex: 1, - result: "\\n0 C3.33 C6.66 C10 C13.33 C16.66 C20 C", - }, - } - - for _, tt := range tests { - d := tt.newDraw() - r, err := drawYAxis(d, tt.newOption(), tt.axisIndex, tt.xAxisHeight, chart.NewBox(10, 10, 10, 10)) - assert.Nil(err) - assert.Equal(&Range{ - divideCount: 6, - Max: 20, - Size: 280, - }, r) - - data, err := d.Bytes() - assert.Nil(err) - assert.Equal(tt.result, string(data)) - } -} From ddd5cf6d4314b254099585d5a51aaef78413b5f8 Mon Sep 17 00:00:00 2001 From: vicanso Date: Mon, 23 May 2022 21:00:10 +0800 Subject: [PATCH 004/116] refactor: enhance painter --- painter.go | 23 ++---- painter_test.go | 22 +++--- theme.go | 198 +++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 215 insertions(+), 28 deletions(-) diff --git a/painter.go b/painter.go index d762e86..47c494f 100644 --- a/painter.go +++ b/painter.go @@ -38,7 +38,7 @@ type Painter struct { parent *Painter style Style previousStyle Style - theme *Theme + theme ColorPalette } type PainterOptions struct { @@ -92,7 +92,7 @@ func PainterStyleOption(style Style) PainterOption { } // PainterThemeOption sets the theme of draw painter -func PainterThemeOption(theme *Theme) PainterOption { +func PainterThemeOption(theme ColorPalette) PainterOption { return func(p *Painter) { if theme == nil { return @@ -194,6 +194,7 @@ func (p *Painter) SetTextStyle(style Style) { func (p *Painter) RestoreStyle() { p.style = p.previousStyle + p.style.WriteToRenderer(p.render) } // Bytes returns the data of draw canvas @@ -336,13 +337,7 @@ func (p *Painter) SetStrokeColor(color Color) { p.render.SetStrokeColor(color) } -func (p *Painter) LineStroke(points []Point, style LineStyle) { - s := style.Style() - if !s.ShouldDrawStroke() { - return - } - defer p.RestoreStyle() - p.SetDrawingStyle(s.GetStrokeOptions()) +func (p *Painter) LineStroke(points []Point) { for index, point := range points { x := point.X y := point.Y @@ -360,8 +355,9 @@ func (p *Painter) SetBackground(width, height int, color Color) { s := chart.Style{ FillColor: color, } - defer p.RestoreStyle() + // 背景色 p.SetStyle(s) + defer p.RestoreStyle() // 设置背景色不使用box,因此不直接使用Painter r.MoveTo(0, 0) r.LineTo(width, 0) @@ -396,13 +392,8 @@ func (p *Painter) Polygon(center Point, radius float64, sides int) { p.Stroke() } -func (p *Painter) FillArea(points []Point, s Style) { - if !s.ShouldDrawFill() { - return - } - defer p.RestoreStyle() +func (p *Painter) FillArea(points []Point) { var x, y int - p.SetDrawingStyle(s.GetFillOptions()) for index, point := range points { x = point.X y = point.Y diff --git a/painter_test.go b/painter_test.go index 1cc08be..34c0c05 100644 --- a/painter_test.go +++ b/painter_test.go @@ -99,6 +99,10 @@ func TestPainter(t *testing.T) { // line stroke { fn: func(p *Painter) { + p.SetDrawingStyle(Style{ + StrokeColor: drawing.ColorBlack, + StrokeWidth: 1, + }) p.LineStroke([]Point{ { X: 1, @@ -108,9 +112,6 @@ func TestPainter(t *testing.T) { X: 3, Y: 4, }, - }, LineStyle{ - StrokeColor: drawing.ColorBlack, - StrokeWidth: 1, }) }, result: "\\n", @@ -294,6 +295,14 @@ func TestPainter(t *testing.T) { // FillArea { fn: func(p *Painter) { + p.SetDrawingStyle(Style{ + FillColor: drawing.Color{ + R: 84, + G: 112, + B: 198, + A: 255, + }, + }) p.FillArea([]Point{ { X: 0, @@ -311,13 +320,6 @@ func TestPainter(t *testing.T) { X: 0, Y: 0, }, - }, Style{ - FillColor: drawing.Color{ - R: 84, - G: 112, - B: 198, - A: 255, - }, }) }, result: "\\n", diff --git a/theme.go b/theme.go index 88c73df..bb05249 100644 --- a/theme.go +++ b/theme.go @@ -22,13 +22,20 @@ package charts +import "github.com/wcharczuk/go-chart/v2/drawing" + const ThemeDark = "dark" const ThemeLight = "light" const ThemeGrafana = "grafana" const ThemeAnt = "ant" -type Theme struct { - palette *themeColorPalette +type ColorPalette interface { + IsDark() bool + GetAxisStrokeColor() Color + GetAxisSplitLineColor() Color + GetSeriesColor(int) Color + GetBackgroundColor() Color + GetTextColor() Color } type themeColorPalette struct { @@ -39,3 +46,190 @@ type themeColorPalette struct { textColor Color seriesColors []Color } + +var palettes = map[string]ColorPalette{} + +func init() { + echartSeriesColors := []Color{ + parseColor("#5470c6"), + parseColor("#91cc75"), + parseColor("#fac858"), + parseColor("#ee6666"), + parseColor("#73c0de"), + parseColor("#3ba272"), + parseColor("#fc8452"), + parseColor("#9a60b4"), + parseColor("#ea7ccc"), + } + grafanaSeriesColors := []Color{ + parseColor("#7EB26D"), + parseColor("#EAB839"), + parseColor("#6ED0E0"), + parseColor("#EF843C"), + parseColor("#E24D42"), + parseColor("#1F78C1"), + parseColor("#705DA0"), + parseColor("#508642"), + } + antSeriesColors := []Color{ + parseColor("#5b8ff9"), + parseColor("#5ad8a6"), + parseColor("#5d7092"), + parseColor("#f6bd16"), + parseColor("#6f5ef9"), + parseColor("#6dc8ec"), + parseColor("#945fb9"), + parseColor("#ff9845"), + } + AddTheme( + ThemeDark, + true, + Color{ + R: 185, + G: 184, + B: 206, + A: 255, + }, + Color{ + R: 72, + G: 71, + B: 83, + A: 255, + }, + Color{ + R: 16, + G: 12, + B: 42, + A: 255, + }, + Color{ + R: 238, + G: 238, + B: 238, + A: 255, + }, + echartSeriesColors, + ) + + AddTheme( + ThemeLight, + false, + Color{ + R: 110, + G: 112, + B: 121, + A: 255, + }, + Color{ + R: 224, + G: 230, + B: 242, + A: 255, + }, + drawing.ColorWhite, + drawing.Color{ + R: 70, + G: 70, + B: 70, + A: 255, + }, + echartSeriesColors, + ) + AddTheme( + ThemeAnt, + false, + Color{ + R: 110, + G: 112, + B: 121, + A: 255, + }, + Color{ + R: 224, + G: 230, + B: 242, + A: 255, + }, + drawing.ColorWhite, + drawing.Color{ + R: 70, + G: 70, + B: 70, + A: 255, + }, + antSeriesColors, + ) + AddTheme( + ThemeGrafana, + true, + drawing.Color{ + R: 185, + G: 184, + B: 206, + A: 255, + }, + drawing.Color{ + R: 68, + G: 67, + B: 67, + A: 255, + }, + drawing.Color{ + R: 31, + G: 29, + B: 29, + A: 255, + }, + drawing.Color{ + R: 216, + G: 217, + B: 218, + A: 255, + }, + grafanaSeriesColors, + ) +} + +func AddTheme(name string, isDarkMode bool, axisStrokeColor, axisSplitLineColor, backgroundColor, textColor drawing.Color, seriesColors []drawing.Color) { + palettes[name] = &themeColorPalette{ + isDarkMode: isDarkMode, + axisStrokeColor: axisStrokeColor, + axisSplitLineColor: axisSplitLineColor, + backgroundColor: backgroundColor, + textColor: textColor, + seriesColors: seriesColors, + } +} + +func NewTheme(name string) ColorPalette { + p, ok := palettes[name] + if !ok { + p = palettes[ThemeLight] + } + return p +} + +func (t *themeColorPalette) IsDark() bool { + return t.isDarkMode +} + +func (t *themeColorPalette) GetAxisStrokeColor() Color { + return t.axisStrokeColor +} + +func (t *themeColorPalette) GetAxisSplitLineColor() Color { + return t.axisSplitLineColor +} + +func (t *themeColorPalette) GetSeriesColor(index int) Color { + colors := t.seriesColors + return colors[index%len(colors)] +} + +func (t *themeColorPalette) GetBackgroundColor() Color { + return t.backgroundColor +} + +func (t *themeColorPalette) GetTextColor() Color { + return t.textColor +} From 4201c7d4393caec7f6c15725ead9602274e41d41 Mon Sep 17 00:00:00 2001 From: vicanso Date: Tue, 24 May 2022 23:25:08 +0800 Subject: [PATCH 005/116] chore: support axias ticks render --- painter.go | 217 +++++++++++++++++++++++++++++++++++------------- painter_test.go | 2 +- util.go | 15 ++-- 3 files changed, 165 insertions(+), 69 deletions(-) diff --git a/painter.go b/painter.go index 47c494f..971a028 100644 --- a/painter.go +++ b/painter.go @@ -32,13 +32,12 @@ import ( ) type Painter struct { - render Renderer - box Box - font *truetype.Font - parent *Painter - style Style - previousStyle Style - theme ColorPalette + render Renderer + box Box + font *truetype.Font + parent *Painter + style Style + theme ColorPalette } type PainterOptions struct { @@ -54,6 +53,12 @@ type PainterOptions struct { type PainterOption func(*Painter) +type TicksOption struct { + Length int + Orient string + Count int +} + // PainterPaddingOption sets the padding of draw painter func PainterPaddingOption(padding Box) PainterOption { return func(p *Painter) { @@ -87,7 +92,7 @@ func PainterFontOption(font *truetype.Font) PainterOption { // PainterStyleOption sets the style of draw painter func PainterStyleOption(style Style) PainterOption { return func(p *Painter) { - p.SetDrawingStyle(style) + p.SetStyle(style) } } @@ -156,13 +161,12 @@ func (p *Painter) setOptions(opts ...PainterOption) { 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, - theme: p.theme, + render: p.render, + box: p.box.Clone(), + font: p.font, + parent: p, + style: p.style, + theme: p.theme, } child.setOptions(opt...) return child @@ -172,29 +176,65 @@ func (p *Painter) SetStyle(style Style) { if style.Font == nil { style.Font = p.font } - 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) { - if style.Font == nil { - style.Font = p.font +func overrideStyle(defaultStyle Style, style Style) Style { + if style.StrokeWidth == 0 { + style.StrokeWidth = defaultStyle.StrokeWidth } - p.previousStyle = p.style - p.style = style - style.WriteTextOptionsToRenderer(p.render) + 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) RestoreStyle() { - p.style = p.previousStyle +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 { + 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 @@ -208,19 +248,22 @@ func (p *Painter) Bytes() ([]byte, error) { } // MoveTo moves the cursor to a given point -func (p *Painter) MoveTo(x, y int) { +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) { +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) { +func (p *Painter) LineTo(x, y int) *Painter { p.render.LineTo(x+p.box.Left, y+p.box.Top) + return p } -func (p *Painter) Pin(x, y, width int) { +func (p *Painter) Pin(x, y, width int) *Painter { r := float64(width) / 2 y -= width / 4 angle := chart.DegreesToRadians(15) @@ -246,9 +289,10 @@ func (p *Painter) Pin(x, y, width int) { 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) { +func (p *Painter) arrow(x, y, width, height int, direction string) *Painter { halfWidth := width >> 1 halfHeight := height >> 1 if direction == PositionTop || direction == PositionBottom { @@ -284,41 +328,51 @@ func (p *Painter) arrow(x, y, width, height int, direction string) { p.LineTo(x0, y0) } p.FillStroke() + return p } -func (p *Painter) ArrowLeft(x, y, width, height int) { +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) { +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) { +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) { +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) { +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() { +func (p *Painter) Stroke() *Painter { p.render.Stroke() + return p } -func (p *Painter) Close() { +func (p *Painter) Close() *Painter { p.render.Close() + return p } -func (p *Painter) FillStroke() { +func (p *Painter) FillStroke() *Painter { p.render.FillStroke() + return p } -func (p *Painter) Fill() { +func (p *Painter) Fill() *Painter { p.render.Fill() + return p } func (p *Painter) Width() int { @@ -333,11 +387,7 @@ func (p *Painter) MeasureText(text string) Box { return p.render.MeasureText(text) } -func (p *Painter) SetStrokeColor(color Color) { - p.render.SetStrokeColor(color) -} - -func (p *Painter) LineStroke(points []Point) { +func (p *Painter) LineStroke(points []Point) *Painter { for index, point := range points { x := point.X y := point.Y @@ -348,16 +398,17 @@ func (p *Painter) LineStroke(points []Point) { } } p.Stroke() + return p } -func (p *Painter) SetBackground(width, height int, color Color) { +func (p *Painter) SetBackground(width, height int, color Color) *Painter { r := p.render s := chart.Style{ FillColor: color, } // 背景色 - p.SetStyle(s) - defer p.RestoreStyle() + p.SetDrawingStyle(s) + defer p.ResetStyle() // 设置背景色不使用box,因此不直接使用Painter r.MoveTo(0, 0) r.LineTo(width, 0) @@ -365,21 +416,23 @@ func (p *Painter) SetBackground(width, height int, color Color) { r.LineTo(0, height) r.LineTo(0, 0) p.FillStroke() + return p } -func (p *Painter) MarkLine(x, y, width int) { +func (p *Painter) MarkLine(x, y, width int) *Painter { arrowWidth := 16 arrowHeight := 10 endX := x + width - p.Circle(3, x, y) + radius := 3 + p.Circle(3, x+radius, y) p.render.Fill() - p.MoveTo(x+5, y) + p.MoveTo(x+radius*3, y) p.LineTo(endX-arrowWidth, y) p.Stroke() - p.render.SetStrokeDashArray([]float64{}) p.ArrowRight(endX, y, arrowWidth, arrowHeight) + return p } -func (p *Painter) Polygon(center Point, radius float64, sides int) { +func (p *Painter) Polygon(center Point, radius float64, sides int) *Painter { points := getPolygonPoints(center, radius, sides) for i, item := range points { if i == 0 { @@ -390,9 +443,10 @@ func (p *Painter) Polygon(center Point, radius float64, sides int) { } p.LineTo(points[0].X, points[0].Y) p.Stroke() + return p } -func (p *Painter) FillArea(points []Point) { +func (p *Painter) FillArea(points []Point) *Painter { var x, y int for index, point := range points { x = point.X @@ -404,10 +458,12 @@ func (p *Painter) FillArea(points []Point) { } } p.Fill() + return p } -func (p *Painter) Text(body string, x, y int) { +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 { @@ -433,3 +489,46 @@ func (p *Painter) TextFit(body string, x, y, width int) chart.Box { 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 - 1 + width := p.Width() + height := p.Height() + var values []int + if opt.Orient == OrientHorizontal { + values = autoDivide(height, count) + } else { + values = autoDivide(width, count) + } + + for _, value := range values { + if opt.Orient == OrientVertical { + 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 +} diff --git a/painter_test.go b/painter_test.go index 34c0c05..c847aff 100644 --- a/painter_test.go +++ b/painter_test.go @@ -271,7 +271,7 @@ func TestPainter(t *testing.T) { }) p.MarkLine(0, 20, 300) }, - result: "\\n", + result: "\\n", }, // polygon { diff --git a/util.go b/util.go index 5fee163..7306919 100644 --- a/util.go +++ b/util.go @@ -59,19 +59,16 @@ func getDefaultInt(value, defaultValue int) int { } func autoDivide(max, size int) []int { - unit := max / size + unit := float64(max) / float64(size) - rest := max - unit*size values := make([]int, size+1) - value := 0 - for i := 0; i < size; i++ { - values[i] = value - if i < rest { - value++ + for i := 0; i < size+1; i++ { + if i == size { + values[i] = max + } else { + values[i] = int(float64(i) * unit) } - value += unit } - values[size] = max return values } From 1dcd50ba9a06c83f57414e017bdae1d4718d71b0 Mon Sep 17 00:00:00 2001 From: vicanso Date: Wed, 25 May 2022 23:09:33 +0800 Subject: [PATCH 006/116] feat: support multi text draw --- painter.go | 74 +++++++++++++++++++++++++++++++++++++++++++++++----- util_test.go | 12 ++++----- 2 files changed, 74 insertions(+), 12 deletions(-) diff --git a/painter.go b/painter.go index 971a028..37c60bd 100644 --- a/painter.go +++ b/painter.go @@ -57,6 +57,15 @@ type TicksOption struct { Length int Orient string Count int + Unit int +} + +type MultiTextOption struct { + TextList []string + Orient string + Unit int + Position string + Align string } // PainterPaddingOption sets the padding of draw painter @@ -223,6 +232,9 @@ func (p *Painter) SetDrawingStyle(style Style) *Painter { } func (p *Painter) SetTextStyle(style Style) *Painter { + if style.Font == nil { + style.Font = p.font + } style.WriteTextOptionsToRenderer(p.render) return p } @@ -494,18 +506,25 @@ func (p *Painter) Ticks(opt TicksOption) *Painter { if opt.Count <= 0 || opt.Length <= 0 { return p } - count := opt.Count - 1 + count := opt.Count width := p.Width() height := p.Height() + unit := 1 + if opt.Unit > 1 { + unit = opt.Unit + } var values []int - if opt.Orient == OrientHorizontal { + isVertical := opt.Orient == OrientVertical + if isVertical { values = autoDivide(height, count) } else { values = autoDivide(width, count) } - - for _, value := range values { - if opt.Orient == OrientVertical { + for index, value := range values { + if index%unit != 0 { + continue + } + if isVertical { p.LineStroke([]Point{ { X: 0, @@ -529,6 +548,49 @@ func (p *Painter) Ticks(opt TicksOption) *Painter { }) } } - + return p +} + +func (p *Painter) MultiText(opt MultiTextOption) *Painter { + if len(opt.TextList) == 0 { + return p + } + count := len(opt.TextList) + positionCenter := true + if opt.Position == PositionLeft { + positionCenter = false + } + 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 } diff --git a/util_test.go b/util_test.go index fefbabc..b25c60d 100644 --- a/util_test.go +++ b/util_test.go @@ -60,12 +60,12 @@ func TestAutoDivide(t *testing.T) { assert.Equal([]int{ 0, - 86, - 172, - 258, - 344, - 430, - 515, + 85, + 171, + 257, + 342, + 428, + 514, 600, }, autoDivide(600, 7)) } From 7e4de64a0ddca62a275c47f8085b202cc92d1f05 Mon Sep 17 00:00:00 2001 From: vicanso Date: Thu, 26 May 2022 23:21:02 +0800 Subject: [PATCH 007/116] feat: support grid render function --- alias.go | 6 ++++ painter.go | 86 +++++++++++++++++++++++++++++++++++++++++++++++++++++- util.go | 18 ++++++++++++ 3 files changed, 109 insertions(+), 1 deletion(-) diff --git a/alias.go b/alias.go index 3bacc67..d19c1f9 100644 --- a/alias.go +++ b/alias.go @@ -58,6 +58,12 @@ const ( PositionBottom = "bottom" ) +const ( + AlignLeft = "left" + AlignRight = "right" + AlignCenter = "center" +) + const ( OrientHorizontal = "horizontal" OrientVertical = "vertical" diff --git a/painter.go b/painter.go index 37c60bd..0120d68 100644 --- a/painter.go +++ b/painter.go @@ -68,6 +68,15 @@ type MultiTextOption struct { 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) { @@ -275,6 +284,11 @@ func (p *Painter) LineTo(x, y int) *Painter { 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 @@ -413,6 +427,27 @@ func (p *Painter) LineStroke(points []Point) *Painter { 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{ @@ -557,8 +592,12 @@ func (p *Painter) MultiText(opt MultiTextOption) *Painter { } count := len(opt.TextList) positionCenter := true - if opt.Position == PositionLeft { + if containsString([]string{ + PositionLeft, + PositionTop, + }, opt.Position) { positionCenter = false + count-- } width := p.Width() height := p.Height() @@ -594,3 +633,48 @@ func (p *Painter) MultiText(opt MultiTextOption) *Painter { } 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 +} diff --git a/util.go b/util.go index 7306919..adfa9fd 100644 --- a/util.go +++ b/util.go @@ -43,6 +43,24 @@ func FalseFlag() *bool { return &f } +func containsInt(values []int, value int) bool { + for _, v := range values { + if v == value { + return true + } + } + return false +} + +func containsString(values []string, value string) bool { + for _, v := range values { + if v == value { + return true + } + } + return false +} + func ceilFloatToInt(value float64) int { i := int(value) if value == float64(i) { From 8314a2cb372e8ba852c841969b822b3bfbcb1f91 Mon Sep 17 00:00:00 2001 From: vicanso Date: Tue, 31 May 2022 20:25:14 +0800 Subject: [PATCH 008/116] feat: support dots render function --- examples/painter/main.go | 384 +++++++++++++++++++++++++++++++++++++++ line.go | 46 ----- painter.go | 9 + 3 files changed, 393 insertions(+), 46 deletions(-) create mode 100644 examples/painter/main.go delete mode 100644 line.go diff --git a/examples/painter/main.go b/examples/painter/main.go new file mode 100644 index 0000000..d0eec73 --- /dev/null +++ b/examples/painter/main.go @@ -0,0 +1,384 @@ +package main + +import ( + "io/ioutil" + "os" + "path/filepath" + + charts "github.com/vicanso/go-charts" + "github.com/wcharczuk/go-chart/v2/drawing" +) + +func writeFile(buf []byte) error { + tmpPath := "./tmp" + err := os.MkdirAll(tmpPath, 0700) + if err != nil { + return err + } + + file := filepath.Join(tmpPath, "painter.png") + err = ioutil.WriteFile(file, buf, 0600) + if err != nil { + return err + } + return nil +} + +func main() { + p, err := charts.NewPainter(charts.PainterOptions{ + Width: 400, + Height: 1200, + Type: charts.ChartOutputPNG, + }) + if err != nil { + panic(err) + } + // 背景色 + p.SetBackground(p.Width(), p.Height(), drawing.ColorWhite) + + top := 0 + + // 画线 + p.SetDrawingStyle(charts.Style{ + StrokeColor: drawing.ColorBlack, + FillColor: drawing.ColorBlack, + StrokeWidth: 1, + }) + p.LineStroke([]charts.Point{ + { + X: 0, + Y: 0, + }, + { + X: 100, + Y: 10, + }, + { + X: 200, + Y: 0, + }, + { + X: 300, + Y: 10, + }, + }) + + // 圆滑曲线 + // top += 50 + // p.Child(charts.PainterPaddingOption(charts.Box{ + // Top: top, + // })).SetDrawingStyle(charts.Style{ + // StrokeColor: drawing.ColorBlack, + // FillColor: drawing.ColorBlack, + // StrokeWidth: 1, + // }).SmoothLineStroke([]charts.Point{ + // { + // X: 0, + // Y: 0, + // }, + // { + // X: 100, + // Y: 10, + // }, + // { + // X: 200, + // Y: 0, + // }, + // { + // X: 300, + // Y: 10, + // }, + // }) + + // 标线 + top += 50 + p.Child(charts.PainterPaddingOption(charts.Box{ + Top: top, + })).SetDrawingStyle(charts.Style{ + StrokeColor: drawing.ColorBlack, + FillColor: drawing.ColorBlack, + StrokeWidth: 1, + StrokeDashArray: []float64{ + 4, + 2, + }, + }).MarkLine(0, 0, p.Width()) + + top += 60 + // Polygon + p.Child(charts.PainterBoxOption(charts.Box{ + Top: top, + })).SetDrawingStyle(charts.Style{ + StrokeColor: drawing.ColorBlack, + FillColor: drawing.ColorBlack, + StrokeWidth: 1, + }).Polygon(charts.Point{ + X: 100, + Y: 0, + }, 50, 6) + + // FillArea + top += 60 + p.Child(charts.PainterPaddingOption(charts.Box{ + Top: top, + })).SetDrawingStyle(charts.Style{ + FillColor: drawing.ColorBlack, + }).FillArea([]charts.Point{ + { + X: 0, + Y: 0, + }, + { + X: 100, + Y: 0, + }, + { + X: 150, + Y: 40, + }, + { + X: 80, + Y: 30, + }, + { + X: 0, + Y: 0, + }, + }) + + // 坐标轴的点 + top += 50 + p.Child( + charts.PainterBoxOption(charts.Box{ + Top: top, + Left: 1, + Right: p.Width() - 1, + Bottom: 20, + }), + ).SetDrawingStyle(charts.Style{ + StrokeColor: drawing.ColorBlack, + FillColor: drawing.ColorBlack, + StrokeWidth: 1, + }).Ticks(charts.TicksOption{ + Count: 7, + Length: 5, + }) + + // 坐标轴的点,每2格显示一个 + top += 20 + p.Child( + charts.PainterBoxOption(charts.Box{ + Top: top, + Left: 1, + Right: p.Width() - 1, + Bottom: 20, + }), + ).SetDrawingStyle(charts.Style{ + StrokeColor: drawing.ColorBlack, + FillColor: drawing.ColorBlack, + StrokeWidth: 1, + }).Ticks(charts.TicksOption{ + Unit: 2, + Count: 7, + Length: 5, + }) + + // 坐标轴的点,纵向 + top += 20 + p.Child( + charts.PainterBoxOption(charts.Box{ + Top: top, + Left: 1, + Right: p.Width() - 1, + Bottom: top + 100, + }), + ).SetDrawingStyle(charts.Style{ + StrokeColor: drawing.ColorBlack, + FillColor: drawing.ColorBlack, + StrokeWidth: 1, + }).Ticks(charts.TicksOption{ + Orient: charts.OrientVertical, + Count: 7, + Length: 5, + }) + + // 横向展示文本 + top += 120 + p.Child( + charts.PainterBoxOption(charts.Box{ + Top: top, + Left: 1, + Right: p.Width() - 1, + Bottom: 20, + }), + ).OverrideTextStyle(charts.Style{ + FontColor: drawing.ColorBlack, + FontSize: 10, + }).MultiText(charts.MultiTextOption{ + TextList: []string{ + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun", + }, + }) + + // 横向显示文本,靠左 + top += 20 + p.Child( + charts.PainterBoxOption(charts.Box{ + Top: top, + Left: 1, + Right: p.Width() - 1, + Bottom: 20, + }), + ).OverrideTextStyle(charts.Style{ + FontColor: drawing.ColorBlack, + FontSize: 10, + }).MultiText(charts.MultiTextOption{ + Position: charts.PositionLeft, + TextList: []string{ + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun", + }, + }) + + // 纵向显示文本 + top += 20 + p.Child( + charts.PainterBoxOption(charts.Box{ + Top: top, + Left: 1, + Right: 50, + Bottom: top + 150, + }), + ).OverrideTextStyle(charts.Style{ + FontColor: drawing.ColorBlack, + FontSize: 10, + }).MultiText(charts.MultiTextOption{ + Orient: charts.OrientVertical, + Align: charts.AlignRight, + TextList: []string{ + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun", + }, + }) + // 纵向 文本居中 + p.Child( + charts.PainterBoxOption(charts.Box{ + Top: top, + Left: 50, + Right: 100, + Bottom: top + 150, + }), + ).OverrideTextStyle(charts.Style{ + FontColor: drawing.ColorBlack, + FontSize: 10, + }).MultiText(charts.MultiTextOption{ + Orient: charts.OrientVertical, + Align: charts.AlignCenter, + TextList: []string{ + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun", + }, + }) + // 纵向 文本置顶 + p.Child( + charts.PainterBoxOption(charts.Box{ + Top: top, + Left: 100, + Right: 150, + Bottom: top + 150, + }), + ).OverrideTextStyle(charts.Style{ + FontColor: drawing.ColorBlack, + FontSize: 10, + }).MultiText(charts.MultiTextOption{ + Orient: charts.OrientVertical, + Position: charts.PositionTop, + Align: charts.AlignCenter, + TextList: []string{ + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun", + }, + }) + + // grid + top += 150 + p.Child( + charts.PainterBoxOption(charts.Box{ + Top: top, + Left: 1, + Right: p.Width() - 1, + Bottom: top + 100, + }), + ).OverrideTextStyle(charts.Style{ + FontColor: drawing.ColorBlack, + FontSize: 10, + }).Grid(charts.GridOption{ + Column: 8, + IgnoreColumnLines: []int{0, 8}, + Row: 8, + IgnoreRowLines: []int{0, 8}, + }) + + // dots + top += 100 + p.Child( + charts.PainterBoxOption(charts.Box{ + Top: top, + Left: 1, + Right: p.Width() - 1, + Bottom: top + 20, + }), + ).OverrideDrawingStyle(charts.Style{ + FillColor: drawing.ColorWhite, + StrokeColor: drawing.ColorBlack, + StrokeWidth: 1, + }).Dots([]charts.Point{ + { + X: 0, + Y: 0, + }, + { + X: 50, + Y: 0, + }, + { + X: 100, + Y: 10, + }, + }) + + buf, err := p.Bytes() + if err != nil { + panic(err) + } + err = writeFile(buf) + if err != nil { + panic(err) + } +} diff --git a/line.go b/line.go deleted file mode 100644 index e4b1f18..0000000 --- a/line.go +++ /dev/null @@ -1,46 +0,0 @@ -// 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 - -type LineStyle struct { - ClassName string - StrokeDashArray []float64 - StrokeColor Color - StrokeWidth float64 - FillColor Color - DotWidth float64 - DotColor Color - DotFillColor Color -} - -func (ls *LineStyle) Style() Style { - return Style{ - ClassName: ls.ClassName, - StrokeDashArray: ls.StrokeDashArray, - StrokeColor: ls.StrokeColor, - StrokeWidth: ls.StrokeWidth, - FillColor: ls.FillColor, - DotWidth: ls.DotWidth, - DotColor: ls.DotColor, - } -} diff --git a/painter.go b/painter.go index 0120d68..78d8925 100644 --- a/painter.go +++ b/painter.go @@ -124,6 +124,7 @@ func PainterThemeOption(theme ColorPalette) PainterOption { } } +// PainterWidthHeightOption set width or height of draw painter func PainterWidthHeightOption(width, height int) PainterOption { return func(p *Painter) { if width > 0 { @@ -678,3 +679,11 @@ func (p *Painter) Grid(opt GridOption) *Painter { } return p } + +func (p *Painter) Dots(points []Point) *Painter { + for _, item := range points { + p.Circle(5, item.X, item.Y) + } + p.FillStroke() + return p +} From 622bd8491b778aeeec7209b76e36f93415ad88ac Mon Sep 17 00:00:00 2001 From: vicanso Date: Wed, 1 Jun 2022 20:27:46 +0800 Subject: [PATCH 009/116] feat: support rect and legend line point render --- examples/painter/main.go | 36 ++++++++++++++++++++++++++++++++++++ painter.go | 26 ++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/examples/painter/main.go b/examples/painter/main.go index d0eec73..8b0a157 100644 --- a/examples/painter/main.go +++ b/examples/painter/main.go @@ -373,6 +373,42 @@ func main() { }, }) + // rect + top += 30 + p.Child( + charts.PainterBoxOption(charts.Box{ + Top: top, + Left: 1, + Right: 200, + Bottom: top + 50, + }), + ).OverrideDrawingStyle(charts.Style{ + StrokeColor: drawing.ColorBlack, + FillColor: drawing.ColorBlack, + }).Rect(charts.Box{ + Left: 10, + Top: 0, + Right: 110, + Bottom: 20, + }) + // legend line dot + p.Child( + charts.PainterBoxOption(charts.Box{ + Top: top, + Left: 200, + Right: p.Width() - 1, + Bottom: top + 50, + }), + ).OverrideDrawingStyle(charts.Style{ + StrokeColor: drawing.ColorBlack, + FillColor: drawing.ColorBlack, + }).LegendLineDot(charts.Box{ + Left: 10, + Top: 0, + Right: 50, + Bottom: 20, + }) + buf, err := p.Bytes() if err != nil { panic(err) diff --git a/painter.go b/painter.go index 78d8925..95469f3 100644 --- a/painter.go +++ b/painter.go @@ -687,3 +687,29 @@ func (p *Painter) Dots(points []Point) *Painter { p.FillStroke() return p } + +func (p *Painter) Rect(box Box) *Painter { + p.MoveTo(box.Left, box.Top) + p.LineTo(box.Right, box.Top) + p.LineTo(box.Right, box.Bottom) + p.LineTo(box.Left, box.Bottom) + p.LineTo(box.Left, box.Top) + p.FillStroke() + return p +} + +func (p *Painter) LegendLineDot(box Box) *Painter { + width := box.Width() + height := box.Height() + strokeWidth := 3 + dotHeight := 5 + + p.render.SetStrokeWidth(float64(strokeWidth)) + center := (height - strokeWidth) >> 1 + p.MoveTo(box.Left, box.Top+center) + p.LineTo(box.Right, box.Top+center) + p.Stroke() + p.Circle(float64(dotHeight), box.Left+width>>1, center) + p.FillStroke() + return p +} From 7ee13fe9143b34280b12af28efc52602b9482fb8 Mon Sep 17 00:00:00 2001 From: vicanso Date: Fri, 3 Jun 2022 21:06:40 +0800 Subject: [PATCH 010/116] chore: supper grid renderer --- alias.go | 1 - charts.go | 27 ++++++++++++++ examples/painter/main.go | 13 +++++++ grid.go | 80 ++++++++++++++++++++++++++++++++++++++++ painter.go | 2 +- 5 files changed, 121 insertions(+), 2 deletions(-) create mode 100644 charts.go create mode 100644 grid.go diff --git a/alias.go b/alias.go index d19c1f9..0b161e6 100644 --- a/alias.go +++ b/alias.go @@ -28,7 +28,6 @@ import ( ) type Box = chart.Box -type Renderer = chart.Renderer type Style = chart.Style type Color = drawing.Color diff --git a/charts.go b/charts.go new file mode 100644 index 0000000..445dd7e --- /dev/null +++ b/charts.go @@ -0,0 +1,27 @@ +// 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 + +type Renderer interface { + Render() (Box, error) +} diff --git a/examples/painter/main.go b/examples/painter/main.go index 8b0a157..4614f10 100644 --- a/examples/painter/main.go +++ b/examples/painter/main.go @@ -409,6 +409,19 @@ func main() { Bottom: 20, }) + top += 50 + charts.NewGridPainter(p.Child(charts.PainterBoxOption(charts.Box{ + Top: top, + Left: 1, + Right: p.Width() - 1, + Bottom: top + 100, + })), charts.GridPainterOption{ + Row: 5, + IgnoreFirstRow: true, + IgnoreLastRow: true, + StrokeColor: drawing.ColorBlue, + }).Render() + buf, err := p.Bytes() if err != nil { panic(err) diff --git a/grid.go b/grid.go new file mode 100644 index 0000000..1a00381 --- /dev/null +++ b/grid.go @@ -0,0 +1,80 @@ +// 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 + +type GridPainter struct { + p *Painter + opt *GridPainterOption +} + +type GridPainterOption struct { + StrokeWidth float64 + StrokeColor Color + Column int + Row int + IgnoreFirstRow bool + IgnoreLastRow bool + IgnoreFirstColumn bool + IgnoreLastColumn bool +} + +func NewGridPainter(p *Painter, opt GridPainterOption) *GridPainter { + return &GridPainter{ + p: p, + opt: &opt, + } +} + +func (g *GridPainter) Render() (Box, error) { + opt := g.opt + ignoreColumnLines := make([]int, 0) + if opt.IgnoreFirstColumn { + ignoreColumnLines = append(ignoreColumnLines, 0) + } + if opt.IgnoreLastColumn { + ignoreColumnLines = append(ignoreColumnLines, opt.Column) + } + ignoreRowLines := make([]int, 0) + if opt.IgnoreFirstRow { + ignoreRowLines = append(ignoreRowLines, 0) + } + if opt.IgnoreLastRow { + ignoreRowLines = append(ignoreRowLines, opt.Row) + } + strokeWidth := opt.StrokeWidth + if strokeWidth <= 0 { + strokeWidth = 1 + } + + g.p.SetDrawingStyle(Style{ + StrokeWidth: strokeWidth, + StrokeColor: opt.StrokeColor, + }) + g.p.Grid(GridOption{ + Column: opt.Column, + Row: opt.Row, + IgnoreColumnLines: ignoreColumnLines, + IgnoreRowLines: ignoreRowLines, + }) + return g.p.box, nil +} diff --git a/painter.go b/painter.go index 95469f3..851fe11 100644 --- a/painter.go +++ b/painter.go @@ -32,7 +32,7 @@ import ( ) type Painter struct { - render Renderer + render chart.Renderer box Box font *truetype.Font parent *Painter From 4cf494088e029f30563ff40ab7dbd799b9c00f39 Mon Sep 17 00:00:00 2001 From: vicanso Date: Tue, 7 Jun 2022 23:04:39 +0800 Subject: [PATCH 011/116] feat: support legend render --- examples/painter/main.go | 56 +++++++++++++- legend.go | 154 +++++++++++++++++++++++++++++++++++++++ painter.go | 11 ++- 3 files changed, 216 insertions(+), 5 deletions(-) create mode 100644 legend.go diff --git a/examples/painter/main.go b/examples/painter/main.go index 4614f10..094b98e 100644 --- a/examples/painter/main.go +++ b/examples/painter/main.go @@ -26,7 +26,7 @@ func writeFile(buf []byte) error { func main() { p, err := charts.NewPainter(charts.PainterOptions{ - Width: 400, + Width: 600, Height: 1200, Type: charts.ChartOutputPNG, }) @@ -422,6 +422,60 @@ func main() { StrokeColor: drawing.ColorBlue, }).Render() + top += 100 + charts.NewLegendPainter(p.Child(charts.PainterBoxOption(charts.Box{ + Top: top, + Left: 1, + Right: p.Width() - 1, + Bottom: top + 30, + })), charts.LegendPainterOption{ + Data: []string{ + "Email", + "Union Ads", + "Video Ads", + "Direct", + }, + FontSize: 12, + FontColor: drawing.ColorBlack, + }).Render() + + top += 30 + charts.NewLegendPainter(p.Child(charts.PainterBoxOption(charts.Box{ + Top: top, + Left: 1, + Right: p.Width() - 1, + Bottom: top + 30, + })), charts.LegendPainterOption{ + Data: []string{ + "Email", + "Union Ads", + "Video Ads", + "Direct", + }, + Align: charts.AlignRight, + FontSize: 16, + Icon: charts.IconRect, + FontColor: drawing.ColorBlack, + }).Render() + + top += 30 + charts.NewLegendPainter(p.Child(charts.PainterBoxOption(charts.Box{ + Top: top, + Left: 1, + Right: p.Width() - 1, + Bottom: top + 100, + })), charts.LegendPainterOption{ + Data: []string{ + "Email", + "Union Ads", + "Video Ads", + "Direct", + }, + Orient: charts.OrientVertical, + FontSize: 12, + FontColor: drawing.ColorBlack, + }).Render() + buf, err := p.Bytes() if err != nil { panic(err) diff --git a/legend.go b/legend.go new file mode 100644 index 0000000..d128272 --- /dev/null +++ b/legend.go @@ -0,0 +1,154 @@ +// 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 + +type LegendPainter struct { + p *Painter + opt *LegendPainterOption +} + +const IconRect = "rect" +const IconLineDot = "lineDot" + +type LegendPainterOption struct { + Theme ColorPalette + // Text array of legend + Data []string + // Distance between legend component and the left side of the container. + // It can be pixel value: 20, percentage value: 20%, + // or position value: right, center. + Left string + // Distance between legend component and the top side of the container. + // It can be pixel value: 20. + Top string + // Legend marker and text aligning, it can be left or right, default is left. + Align string + // The layout orientation of legend, it can be horizontal or vertical, default is horizontal. + Orient string + // Icon of the legend. + Icon string + // Font size of legend text + FontSize float64 + // FontColor color of legend text + FontColor Color +} + +func NewLegendPainter(p *Painter, opt LegendPainterOption) *LegendPainter { + return &LegendPainter{ + p: p, + opt: &opt, + } +} + +func (l *LegendPainter) Render() (Box, error) { + opt := l.opt + theme := opt.Theme + if theme == nil { + theme = l.p.theme + } + p := l.p + p.SetTextStyle(Style{ + FontSize: opt.FontSize, + FontColor: opt.FontColor, + }) + measureList := make([]Box, len(opt.Data)) + maxTextWidth := 0 + for index, text := range opt.Data { + b := p.MeasureText(text) + if b.Width() > maxTextWidth { + maxTextWidth = b.Width() + } + measureList[index] = b + } + x := 0 + y := 0 + offset := 20 + textOffset := 2 + legendWidth := 30 + legendHeight := 20 + drawIcon := func(top, left int) int { + if opt.Icon == IconRect { + p.Rect(Box{ + Top: top - legendHeight + 4, + Left: left, + Right: left + legendWidth, + Bottom: top - 2, + }) + } else { + p.LegendLineDot(Box{ + Top: top, + Left: left, + Right: left + legendWidth, + Bottom: top + legendHeight, + }) + } + return left + legendWidth + } + for index, text := range opt.Data { + color := theme.GetSeriesColor(index) + p.SetDrawingStyle(Style{ + FillColor: color, + StrokeColor: color, + }) + if opt.Align != AlignRight { + x = drawIcon(y, x) + x += textOffset + } + p.Text(text, x, y) + x += measureList[index].Width() + if opt.Align == AlignRight { + x += textOffset + x = drawIcon(0, x) + } + if opt.Orient == OrientVertical { + y += offset + x = 0 + } else { + x += offset + y = 0 + } + } + 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{ + Right: width, + Bottom: height, + }, nil +} diff --git a/painter.go b/painter.go index 851fe11..61a8e95 100644 --- a/painter.go +++ b/painter.go @@ -170,6 +170,9 @@ func NewPainter(opts PainterOptions, opt ...PainterOption) (*Painter, error) { font: font, } p.setOptions(opt...) + if p.theme == nil { + p.theme = NewTheme(ThemeLight) + } return p, nil } func (p *Painter) setOptions(opts ...PainterOption) { @@ -705,11 +708,11 @@ func (p *Painter) LegendLineDot(box Box) *Painter { dotHeight := 5 p.render.SetStrokeWidth(float64(strokeWidth)) - center := (height - strokeWidth) >> 1 - p.MoveTo(box.Left, box.Top+center) - p.LineTo(box.Right, box.Top+center) + center := (height-strokeWidth)>>1 - 1 + p.MoveTo(box.Left, box.Top-center) + p.LineTo(box.Right, box.Top-center) p.Stroke() - p.Circle(float64(dotHeight), box.Left+width>>1, center) + p.Circle(float64(dotHeight), box.Left+width>>1, box.Top-center) p.FillStroke() return p } From b394e1b49f9965a06b3be4e9121301fad81b144a Mon Sep 17 00:00:00 2001 From: vicanso Date: Wed, 8 Jun 2022 23:19:03 +0800 Subject: [PATCH 012/116] feat: support axis render --- axis.go | 192 +++++++++++++++++++++++++++++++++++++++ examples/painter/main.go | 71 ++++++++++++++- grid.go | 8 +- legend.go | 97 +++++++++++++------- painter.go | 15 +++ 5 files changed, 344 insertions(+), 39 deletions(-) create mode 100644 axis.go diff --git a/axis.go b/axis.go new file mode 100644 index 0000000..d2b559b --- /dev/null +++ b/axis.go @@ -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 +} diff --git a/examples/painter/main.go b/examples/painter/main.go index 094b98e..acbb3ef 100644 --- a/examples/painter/main.go +++ b/examples/painter/main.go @@ -27,7 +27,7 @@ func writeFile(buf []byte) error { func main() { p, err := charts.NewPainter(charts.PainterOptions{ Width: 600, - Height: 1200, + Height: 2000, Type: charts.ChartOutputPNG, }) if err != nil { @@ -429,6 +429,7 @@ func main() { Right: p.Width() - 1, Bottom: top + 30, })), charts.LegendPainterOption{ + Left: "10", Data: []string{ "Email", "Union Ads", @@ -446,6 +447,7 @@ func main() { Right: p.Width() - 1, Bottom: top + 30, })), charts.LegendPainterOption{ + Left: charts.PositionRight, Data: []string{ "Email", "Union Ads", @@ -465,6 +467,7 @@ func main() { Right: p.Width() - 1, Bottom: top + 100, })), charts.LegendPainterOption{ + Top: "10", Data: []string{ "Email", "Union Ads", @@ -476,6 +479,72 @@ func main() { FontColor: drawing.ColorBlack, }).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() if err != nil { panic(err) diff --git a/grid.go b/grid.go index 1a00381..252fe2e 100644 --- a/grid.go +++ b/grid.go @@ -22,7 +22,7 @@ package charts -type GridPainter struct { +type gridPainter struct { p *Painter opt *GridPainterOption } @@ -38,14 +38,14 @@ type GridPainterOption struct { IgnoreLastColumn bool } -func NewGridPainter(p *Painter, opt GridPainterOption) *GridPainter { - return &GridPainter{ +func NewGridPainter(p *Painter, opt GridPainterOption) *gridPainter { + return &gridPainter{ p: p, opt: &opt, } } -func (g *GridPainter) Render() (Box, error) { +func (g *gridPainter) Render() (Box, error) { opt := g.opt ignoreColumnLines := make([]int, 0) if opt.IgnoreFirstColumn { diff --git a/legend.go b/legend.go index d128272..16341e4 100644 --- a/legend.go +++ b/legend.go @@ -22,7 +22,12 @@ package charts -type LegendPainter struct { +import ( + "strconv" + "strings" +) + +type legendPainter struct { p *Painter opt *LegendPainterOption } @@ -53,14 +58,14 @@ type LegendPainterOption struct { FontColor Color } -func NewLegendPainter(p *Painter, opt LegendPainterOption) *LegendPainter { - return &LegendPainter{ +func NewLegendPainter(p *Painter, opt LegendPainterOption) *legendPainter { + return &legendPainter{ p: p, opt: &opt, } } -func (l *LegendPainter) Render() (Box, error) { +func (l *legendPainter) Render() (Box, error) { opt := l.opt theme := opt.Theme if theme == nil { @@ -80,12 +85,54 @@ func (l *LegendPainter) Render() (Box, error) { } measureList[index] = b } - x := 0 - y := 0 + + // 计算展示的宽高 + width := 0 + height := 0 offset := 20 textOffset := 2 legendWidth := 30 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 { if opt.Icon == IconRect { p.Rect(Box{ @@ -111,41 +158,23 @@ func (l *LegendPainter) Render() (Box, error) { StrokeColor: color, }) if opt.Align != AlignRight { - x = drawIcon(y, x) - x += textOffset + x0 = drawIcon(y0, x0) + x0 += textOffset } - p.Text(text, x, y) - x += measureList[index].Width() + p.Text(text, x0, y0) + x0 += measureList[index].Width() if opt.Align == AlignRight { - x += textOffset - x = drawIcon(0, x) + x0 += textOffset + x0 = drawIcon(0, x0) } if opt.Orient == OrientVertical { - y += offset - x = 0 + y0 += offset + x0 = x } else { - x += offset - y = 0 + x0 += offset + 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{ Right: width, diff --git a/painter.go b/painter.go index 61a8e95..fb18510 100644 --- a/painter.go +++ b/painter.go @@ -417,6 +417,21 @@ func (p *Painter) MeasureText(text string) Box { 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 { for index, point := range points { x := point.X From c4045cfbbee1f8b8125ed8ee30e1c90f5f884eb1 Mon Sep 17 00:00:00 2001 From: vicanso Date: Sun, 12 Jun 2022 11:55:37 +0800 Subject: [PATCH 013/116] feat: support line chart render function --- axis.go | 154 ++++++++++++++------ charts.go | 23 +++ examples/line_chart/main.go | 111 +++++++++++++++ examples/painter/main.go | 54 +++++++- line_chart.go | 134 ++++++++++++++++++ painter.go | 4 +- range.go | 127 +++++++++++++++++ series.go | 270 ++++++++++++++++++++++++++++++++++++ theme.go | 27 +++- xaxis.go | 88 ++++++++++++ yaxis.go | 66 +++++++++ 11 files changed, 1012 insertions(+), 46 deletions(-) create mode 100644 examples/line_chart/main.go create mode 100644 line_chart.go create mode 100644 range.go create mode 100644 series.go create mode 100644 xaxis.go create mode 100644 yaxis.go diff --git a/axis.go b/axis.go index d2b559b..bb2e6a3 100644 --- a/axis.go +++ b/axis.go @@ -39,6 +39,8 @@ func NewAxisPainter(p *Painter, opt AxisPainterOption) *axisPainter { } type AxisPainterOption struct { + // The theme of chart + Theme ColorPalette // The label of axis Data []string // The boundary gap on both sides of a coordinate axis. @@ -70,13 +72,31 @@ type AxisPainterOption struct { func (a *axisPainter) Render() (Box, error) { opt := a.opt - p := a.p + top := a.p + theme := opt.Theme strokeWidth := opt.StrokeWidth if strokeWidth == 0 { strokeWidth = 1 } + font := opt.Font + if font == nil { + font = theme.GetFont() + } + fontColor := opt.FontColor + if fontColor.IsZero() { + fontColor = theme.GetTextColor() + } + fontSize := opt.FontSize + if fontSize == 0 { + fontSize = theme.GetFontSize() + } + strokeColor := opt.StrokeColor + if strokeColor.IsZero() { + strokeColor = theme.GetAxisStrokeColor() + } + tickCount := opt.SplitNumber if tickCount == 0 { tickCount = len(opt.Data) @@ -86,12 +106,17 @@ func (a *axisPainter) Render() (Box, error) { if opt.BoundaryGap != nil && !*opt.BoundaryGap { boundaryGap = false } + isVertical := opt.Position == PositionLeft || + opt.Position == PositionRight labelPosition := "" if !boundaryGap { tickCount-- labelPosition = PositionLeft } + if isVertical && boundaryGap { + labelPosition = PositionCenter + } // TODO 计算unit unit := 1 @@ -99,84 +124,104 @@ func (a *axisPainter) Render() (Box, error) { tickLength := getDefaultInt(opt.TickLength, 5) labelMargin := getDefaultInt(opt.LabelMargin, 5) - textMaxWidth, textMaxHeight := p.MeasureTextMaxWidthHeight(opt.Data) + style := Style{ + StrokeColor: strokeColor, + StrokeWidth: strokeWidth, + Font: font, + FontColor: fontColor, + FontSize: fontSize, + } + top.SetDrawingStyle(style).OverrideTextStyle(style) + + textMaxWidth, textMaxHeight := top.MeasureTextMaxWidthHeight(opt.Data) width := 0 height := 0 // 垂直 - if opt.Position == PositionLeft || - opt.Position == PositionRight { + if isVertical { width = textMaxWidth + tickLength<<1 - height = p.Height() + height = top.Height() } else { - width = p.Width() + width = top.Width() height = tickLength<<1 + textMaxHeight } padding := Box{} switch opt.Position { case PositionTop: - padding.Top = p.Height() - height + padding.Top = top.Height() - height case PositionLeft: - padding.Right = p.Width() - width + padding.Right = top.Width() - width + case PositionRight: + padding.Left = top.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, - }) + + p := top.Child(PainterPaddingOption(padding)) x0 := 0 y0 := 0 x1 := 0 y1 := 0 - ticksPadding := 0 - labelPadding := 0 + ticksPaddingTop := 0 + ticksPaddingLeft := 0 + labelPaddingTop := 0 + labelPaddingLeft := 0 + labelPaddingRight := 0 orient := "" textAlign := "" switch opt.Position { case PositionTop: - labelPadding = labelMargin + labelPaddingTop = labelMargin x1 = p.Width() y0 = labelMargin + int(opt.FontSize) - ticksPadding = int(opt.FontSize) + ticksPaddingTop = int(opt.FontSize) y1 = y0 orient = OrientHorizontal case PositionLeft: + x0 = p.Width() + y0 = 0 + x1 = p.Width() + y1 = p.Height() orient = OrientVertical textAlign = AlignRight + ticksPaddingLeft = textMaxWidth + tickLength + labelPaddingRight = width - textMaxWidth + case PositionRight: + orient = OrientVertical + y1 = p.Height() + labelPaddingLeft = width - textMaxWidth default: - labelPadding = height + labelPaddingTop = 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, - }, - }) + if strokeWidth > 0 { + p.Child(PainterPaddingOption(Box{ + Top: ticksPaddingTop, + Left: ticksPaddingLeft, + })).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, + Left: labelPaddingLeft, + Top: labelPaddingTop, + Right: labelPaddingRight, })).MultiText(MultiTextOption{ Align: textAlign, TextList: opt.Data, @@ -184,6 +229,31 @@ func (a *axisPainter) Render() (Box, error) { Unit: unit, Position: labelPosition, }) + // 显示辅助线 + if opt.SplitLineShow { + style.StrokeColor = opt.SplitLineColor + top.OverrideDrawingStyle(style) + if isVertical { + x0 := p.Width() + x1 := top.Width() + if opt.Position == PositionRight { + x0 = 0 + x1 = top.Width() - p.Width() + } + for _, y := range autoDivide(height, tickCount) { + top.LineStroke([]Point{ + { + X: x0, + Y: y, + }, + { + X: x1, + Y: y, + }, + }) + } + } + } return Box{ Bottom: height, diff --git a/charts.go b/charts.go index 445dd7e..591ebea 100644 --- a/charts.go +++ b/charts.go @@ -25,3 +25,26 @@ package charts type Renderer interface { Render() (Box, error) } + +type defaultRenderOption struct { + Theme ColorPalette + Padding Box +} + +func defaultRender(p *Painter, opt defaultRenderOption) *Painter { + p.SetBackground(p.Width(), p.Height(), opt.Theme.GetBackgroundColor()) + if !opt.Padding.IsZero() { + p = p.Child(PainterPaddingOption(opt.Padding)) + } + return p +} + +func doRender(renderers ...Renderer) error { + for _, r := range renderers { + _, err := r.Render() + if err != nil { + return err + } + } + return nil +} diff --git a/examples/line_chart/main.go b/examples/line_chart/main.go new file mode 100644 index 0000000..e15500c --- /dev/null +++ b/examples/line_chart/main.go @@ -0,0 +1,111 @@ +package main + +import ( + "io/ioutil" + "os" + "path/filepath" + + "github.com/vicanso/go-charts" +) + +func writeFile(buf []byte) error { + tmpPath := "./tmp" + err := os.MkdirAll(tmpPath, 0700) + if err != nil { + return err + } + + file := filepath.Join(tmpPath, "line-chart.png") + err = ioutil.WriteFile(file, buf, 0600) + if err != nil { + return err + } + return nil +} + +func main() { + p, err := charts.NewPainter(charts.PainterOptions{ + Width: 800, + Height: 600, + Type: charts.ChartOutputPNG, + }) + if err != nil { + panic(err) + } + _, err = charts.NewLineChart(p, charts.LineChartOption{ + Padding: charts.Box{ + Left: 10, + Top: 10, + Right: 10, + Bottom: 10, + }, + XAxis: charts.NewXAxisOption([]string{ + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun", + }), + SeriesList: charts.SeriesList{ + charts.NewSeriesFromValues([]float64{ + 120, + 132, + 101, + 134, + 90, + 230, + 210, + }), + charts.NewSeriesFromValues([]float64{ + 220, + 182, + 191, + 234, + 290, + 330, + 310, + }), + charts.NewSeriesFromValues([]float64{ + 150, + 232, + 201, + 154, + 190, + 330, + 410, + }), + charts.NewSeriesFromValues([]float64{ + 320, + 332, + 301, + 334, + 390, + 330, + 320, + }), + charts.NewSeriesFromValues([]float64{ + 820, + 932, + 901, + 934, + 1290, + 1330, + 1320, + }), + }, + }).Render() + if err != nil { + panic(err) + } + + buf, err := p.Bytes() + if err != nil { + panic(err) + } + err = writeFile(buf) + if err != nil { + panic(err) + } +} diff --git a/examples/painter/main.go b/examples/painter/main.go index acbb3ef..5022584 100644 --- a/examples/painter/main.go +++ b/examples/painter/main.go @@ -409,6 +409,7 @@ func main() { Bottom: 20, }) + // grid top += 50 charts.NewGridPainter(p.Child(charts.PainterBoxOption(charts.Box{ Top: top, @@ -422,6 +423,7 @@ func main() { StrokeColor: drawing.ColorBlue, }).Render() + // legend top += 100 charts.NewLegendPainter(p.Child(charts.PainterBoxOption(charts.Box{ Top: top, @@ -440,6 +442,7 @@ func main() { FontColor: drawing.ColorBlack, }).Render() + // legend top += 30 charts.NewLegendPainter(p.Child(charts.PainterBoxOption(charts.Box{ Top: top, @@ -460,6 +463,7 @@ func main() { FontColor: drawing.ColorBlack, }).Render() + // legend top += 30 charts.NewLegendPainter(p.Child(charts.PainterBoxOption(charts.Box{ Top: top, @@ -479,6 +483,7 @@ func main() { FontColor: drawing.ColorBlack, }).Render() + // axis bottom top += 100 charts.NewAxisPainter(p.Child(charts.PainterBoxOption(charts.Box{ Top: top, @@ -500,6 +505,7 @@ func main() { FontColor: drawing.ColorBlack, }).Render() + // axis top top += 50 charts.NewAxisPainter(p.Child(charts.PainterBoxOption(charts.Box{ Top: top, @@ -523,11 +529,12 @@ func main() { FontColor: drawing.ColorBlack, }).Render() + // axis left top += 50 charts.NewAxisPainter(p.Child(charts.PainterBoxOption(charts.Box{ Top: top, Left: 10, - Right: p.Width() - 1, + Right: 60, Bottom: top + 200, })), charts.AxisPainterOption{ Position: charts.PositionLeft, @@ -544,6 +551,51 @@ func main() { FontSize: 12, FontColor: drawing.ColorBlack, }).Render() + // axis right + charts.NewAxisPainter(p.Child(charts.PainterBoxOption(charts.Box{ + Top: top, + Left: 100, + Right: 150, + Bottom: top + 200, + })), charts.AxisPainterOption{ + Position: charts.PositionRight, + Data: []string{ + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun", + }, + StrokeColor: drawing.ColorBlack, + FontSize: 12, + FontColor: drawing.ColorBlack, + }).Render() + + // axis left no tick + charts.NewAxisPainter(p.Child(charts.PainterBoxOption(charts.Box{ + Top: top, + Left: 150, + Right: 300, + Bottom: top + 200, + })), charts.AxisPainterOption{ + BoundaryGap: charts.FalseFlag(), + Position: charts.PositionLeft, + Data: []string{ + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun", + }, + FontSize: 12, + FontColor: drawing.ColorBlack, + SplitLineShow: true, + SplitLineColor: drawing.ColorBlack.WithAlpha(100), + }).Render() buf, err := p.Bytes() if err != nil { diff --git a/line_chart.go b/line_chart.go new file mode 100644 index 0000000..9640087 --- /dev/null +++ b/line_chart.go @@ -0,0 +1,134 @@ +// 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 lineChart struct { + p *Painter + opt *LineChartOption +} + +func NewLineChart(p *Painter, opt LineChartOption) *lineChart { + if opt.Theme == nil { + opt.Theme = NewTheme("") + } + return &lineChart{ + p: p, + opt: &opt, + } +} + +type LineChartOption struct { + Theme ColorPalette + // The data series list + SeriesList SeriesList + // The x axis option + XAxis XAxisOption + // The padding of line chart + Padding Box + // The y axis option + YAxis YAxisOption +} + +func (l *lineChart) Render() (Box, error) { + p := l.p + opt := l.opt + p = defaultRender(p, defaultRenderOption{ + Theme: opt.Theme, + Padding: opt.Padding, + }) + + seriesList := opt.SeriesList + seriesList.init() + // 过滤前先计算最大最小值 + max, min := seriesList.GetMaxMin() + + seriesList = seriesList.Filter(ChartTypeLine) + + // Y轴 + yr := NewRange(AxisRangeOption{ + Min: min, + Max: max, + // 高度需要减去x轴的高度 + Size: p.Height() - defaultXAxisHeight, + DivideCount: defaultAxisDivideCount, + }) + if opt.YAxis.Theme == nil { + opt.YAxis.Theme = opt.Theme + } + opt.YAxis.Data = yr.Values() + reverseStringSlice(opt.YAxis.Data) + yAxis := NewLeftYAxis(p, opt.YAxis) + yAxisBox, err := yAxis.Render() + if err != nil { + return chart.BoxZero, err + } + seriesPainter := p.Child(PainterPaddingOption(Box{ + Bottom: defaultXAxisHeight, + Left: yAxisBox.Width(), + })) + + if opt.XAxis.Theme == nil { + opt.XAxis.Theme = opt.Theme + } + xAxis := NewBottomXAxis(p.Child(PainterPaddingOption(Box{ + Left: yAxisBox.Width(), + })), opt.XAxis) + + xDivideValues := autoDivide(seriesPainter.Width(), len(opt.XAxis.Data)) + xValues := make([]int, len(xDivideValues)-1) + for i := 0; i < len(xDivideValues)-1; i++ { + xValues[i] = (xDivideValues[i] + xDivideValues[i+1]) >> 1 + } + for index, series := range seriesList { + seriesColor := opt.Theme.GetSeriesColor(index) + seriesPainter.SetDrawingStyle(Style{ + StrokeColor: seriesColor, + StrokeWidth: 2, + FillColor: seriesColor, + }) + points := make([]Point, 0) + for i, item := range series.Data { + h := yr.getRestHeight(item.Value) + p := Point{ + X: xValues[i], + Y: h, + } + points = append(points, p) + } + seriesPainter.LineStroke(points) + seriesPainter.Dots(points) + } + + err = doRender( + xAxis, + ) + if err != nil { + return chart.BoxZero, err + } + + return p.box, nil +} diff --git a/painter.go b/painter.go index fb18510..75d4a38 100644 --- a/painter.go +++ b/painter.go @@ -636,7 +636,7 @@ func (p *Painter) MultiText(opt MultiTextOption) *Painter { x := 0 y := 0 if isVertical { - y = start - box.Height()>>1 + y = start + box.Height()>>1 switch opt.Align { case AlignRight: x = width - box.Width() @@ -700,7 +700,7 @@ func (p *Painter) Grid(opt GridOption) *Painter { func (p *Painter) Dots(points []Point) *Painter { for _, item := range points { - p.Circle(5, item.X, item.Y) + p.Circle(3, item.X, item.Y) } p.FillStroke() return p diff --git a/range.go b/range.go new file mode 100644 index 0000000..399c449 --- /dev/null +++ b/range.go @@ -0,0 +1,127 @@ +// 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" +) + +const defaultAxisDivideCount = 6 + +type axisRange struct { + divideCount int + min float64 + max float64 + size int + boundary bool +} + +type AxisRangeOption struct { + Min float64 + Max float64 + Size int + Boundary bool + DivideCount int +} + +func NewRange(opt AxisRangeOption) axisRange { + max := opt.Max + min := opt.Min + + max += math.Abs(max * 0.1) + min -= math.Abs(min * 0.1) + divideCount := opt.DivideCount + r := math.Abs(max - min) + + // 最小单位计算 + unit := 2 + if r > 10 { + unit = 4 + } + if r > 30 { + unit = 5 + } + if r > 100 { + unit = 10 + } + if r > 200 { + unit = 20 + } + unit = int((r/float64(divideCount))/float64(unit))*unit + unit + + if min != 0 { + isLessThanZero := min < 0 + min = float64(int(min/float64(unit)) * unit) + // 如果是小于0,int的时候向上取整了,因此调整 + if min < 0 || + (isLessThanZero && min == 0) { + min -= float64(unit) + } + } + max = min + float64(unit*divideCount) + return axisRange{ + divideCount: divideCount, + min: min, + max: max, + size: opt.Size, + boundary: opt.Boundary, + } +} + +func (r axisRange) Values() []string { + offset := (r.max - r.min) / float64(r.divideCount) + values := make([]string, 0) + for i := 0; i <= r.divideCount; i++ { + v := r.min + float64(i)*offset + value := commafWithDigits(v) + values = append(values, value) + } + return values +} + +func (r *axisRange) getHeight(value float64) int { + v := (value - r.min) / (r.max - r.min) + return int(v * float64(r.size)) +} + +func (r *axisRange) getRestHeight(value float64) int { + return r.size - r.getHeight(value) +} + +func (r *axisRange) GetRange(index int) (float64, float64) { + unit := float64(r.size) / float64(r.divideCount) + return unit * float64(index), unit * float64(index+1) +} +func (r *axisRange) AutoDivide() []int { + return autoDivide(r.size, r.divideCount) +} + +func (r *axisRange) 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)) +} diff --git a/series.go b/series.go new file mode 100644 index 0000000..07bd5d9 --- /dev/null +++ b/series.go @@ -0,0 +1,270 @@ +// 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" + "strings" + + "github.com/dustin/go-humanize" + "github.com/wcharczuk/go-chart/v2" +) + +type SeriesData struct { + // The value of series data + Value float64 + // The style of series data + Style Style +} + +func NewSeriesFromValues(values []float64, chartType ...string) Series { + s := Series{ + Data: NewSeriesDataFromValues(values), + } + if len(chartType) != 0 { + s.Type = chartType[0] + } + return s +} + +func NewSeriesDataFromValues(values []float64) []SeriesData { + data := make([]SeriesData, len(values)) + for index, value := range values { + data[index] = SeriesData{ + Value: value, + } + } + return data +} + +type SeriesLabel struct { + // Data label formatter, which supports string template. + // {b}: the name of a data item. + // {c}: the value of a data item. + // {d}: the percent of a data item(pie chart). + Formatter string + // The color for label + Color Color + // Show flag for label + Show bool + // Distance to the host graphic element. + Distance int +} + +const ( + SeriesMarkDataTypeMax = "max" + SeriesMarkDataTypeMin = "min" + SeriesMarkDataTypeAverage = "average" +) + +type SeriesMarkData struct { + // The mark data type, it can be "max", "min", "average". + // The "average" is only for mark line + Type string +} +type SeriesMarkPoint struct { + // The width of symbol, default value is 30 + SymbolSize int + // The mark data of series mark point + Data []SeriesMarkData +} +type SeriesMarkLine struct { + // The mark data of series mark line + Data []SeriesMarkData +} +type Series struct { + index int + // The type of series, it can be "line", "bar" or "pie". + // Default value is "line" + Type string + // The data list of series + Data []SeriesData + // The Y axis index, it should be 0 or 1. + // Default value is 1 + YAxisIndex int + // The style for series + Style chart.Style + // The label for series + Label SeriesLabel + // The name of series + Name string + // Radius for Pie chart, e.g.: 40%, default is "40%" + Radius string + // Mark point for series + MarkPoint SeriesMarkPoint + // Make line for series + MarkLine SeriesMarkLine + // Max value of series + Min *float64 + // Min value of series + Max *float64 +} +type SeriesList []Series + +func (sl SeriesList) init() { + if sl[len(sl)-1].index != 0 { + return + } + for i := 0; i < len(sl); i++ { + if sl[i].Type == "" { + sl[i].Type = ChartTypeLine + } + sl[i].index = i + } +} + +func (sl SeriesList) Filter(chartType string) SeriesList { + arr := make(SeriesList, 0) + for index, item := range sl { + if item.Type == chartType { + arr = append(arr, sl[index]) + } + } + return arr +} + +// GetMaxMin get max and min value of series list +func (sl SeriesList) GetMaxMin() (float64, float64) { + min := math.MaxFloat64 + max := -math.MaxFloat64 + for _, series := range sl { + for _, item := range series.Data { + if item.Value > max { + max = item.Value + } + if item.Value < min { + min = item.Value + } + } + } + return max, min +} + +type PieSeriesOption struct { + Radius string + Label SeriesLabel + Names []string +} + +func NewPieSeriesList(values []float64, opts ...PieSeriesOption) SeriesList { + result := make([]Series, len(values)) + var opt PieSeriesOption + if len(opts) != 0 { + opt = opts[0] + } + for index, v := range values { + name := "" + if index < len(opt.Names) { + name = opt.Names[index] + } + s := Series{ + Type: ChartTypePie, + Data: []SeriesData{ + { + Value: v, + }, + }, + Radius: opt.Radius, + Label: opt.Label, + Name: name, + } + result[index] = s + } + return result +} + +type seriesSummary struct { + MaxIndex int + MaxValue float64 + MinIndex int + MinValue float64 + AverageValue float64 +} + +func (s *Series) Summary() seriesSummary { + minIndex := -1 + maxIndex := -1 + minValue := math.MaxFloat64 + maxValue := -math.MaxFloat64 + sum := float64(0) + for j, item := range s.Data { + if item.Value < minValue { + minIndex = j + minValue = item.Value + } + if item.Value > maxValue { + maxIndex = j + maxValue = item.Value + } + sum += item.Value + } + return seriesSummary{ + MaxIndex: maxIndex, + MaxValue: maxValue, + MinIndex: minIndex, + MinValue: minValue, + AverageValue: sum / float64(len(s.Data)), + } +} + +func (sl SeriesList) Names() []string { + names := make([]string, len(sl)) + for index, s := range sl { + names[index] = s.Name + } + return names +} + +type LabelFormatter func(index int, value float64, percent float64) string + +func NewPieLabelFormatter(seriesNames []string, layout string) LabelFormatter { + if len(layout) == 0 { + layout = "{b}: {d}" + } + return NewLabelFormatter(seriesNames, layout) +} + +func NewValueLabelFormater(seriesNames []string, layout string) LabelFormatter { + if len(layout) == 0 { + layout = "{c}" + } + return NewLabelFormatter(seriesNames, layout) +} + +func NewLabelFormatter(seriesNames []string, layout string) LabelFormatter { + return func(index int, value, percent float64) string { + // 如果无percent的则设置为<0 + percentText := "" + if percent >= 0 { + percentText = humanize.FtoaWithDigits(percent*100, 2) + "%" + } + valueText := humanize.FtoaWithDigits(value, 2) + name := "" + if len(seriesNames) > index { + name = seriesNames[index] + } + text := strings.ReplaceAll(layout, "{c}", valueText) + text = strings.ReplaceAll(text, "{d}", percentText) + text = strings.ReplaceAll(text, "{b}", name) + return text + } +} diff --git a/theme.go b/theme.go index bb05249..544588a 100644 --- a/theme.go +++ b/theme.go @@ -22,7 +22,11 @@ package charts -import "github.com/wcharczuk/go-chart/v2/drawing" +import ( + "github.com/golang/freetype/truetype" + "github.com/wcharczuk/go-chart/v2" + "github.com/wcharczuk/go-chart/v2/drawing" +) const ThemeDark = "dark" const ThemeLight = "light" @@ -36,6 +40,8 @@ type ColorPalette interface { GetSeriesColor(int) Color GetBackgroundColor() Color GetTextColor() Color + GetFontSize() float64 + GetFont() *truetype.Font } type themeColorPalette struct { @@ -45,10 +51,14 @@ type themeColorPalette struct { backgroundColor Color textColor Color seriesColors []Color + fontSize float64 + font *truetype.Font } var palettes = map[string]ColorPalette{} +const defaultFontSize = 12.0 + func init() { echartSeriesColors := []Color{ parseColor("#5470c6"), @@ -233,3 +243,18 @@ func (t *themeColorPalette) GetBackgroundColor() Color { func (t *themeColorPalette) GetTextColor() Color { return t.textColor } + +func (t *themeColorPalette) GetFontSize() float64 { + if t.fontSize != 0 { + return t.fontSize + } + return defaultFontSize +} + +func (t *themeColorPalette) GetFont() *truetype.Font { + if t.font != nil { + return t.font + } + f, _ := chart.GetDefaultFont() + return f +} diff --git a/xaxis.go b/xaxis.go new file mode 100644 index 0000000..a8c28c0 --- /dev/null +++ b/xaxis.go @@ -0,0 +1,88 @@ +// 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 XAxisOption struct { + // The font of x axis + Font *truetype.Font + // The boundary gap on both sides of a coordinate axis. + // Nil or *true means the center part of two axis ticks + BoundaryGap *bool + // The data value of x axis + Data []string + // The theme of chart + Theme ColorPalette + // The font size of x axis label + FontSize float64 + // Hidden x axis + Hidden bool + // Number of segments that the axis is split into. Note that this number serves only as a recommendation. + SplitNumber int + // The position of axis, it can be 'top' or 'bottom' + Position string + // The line color of axis + StrokeColor Color + // The color of label + FontColor Color +} + +const defaultXAxisHeight = 30 + +func NewXAxisOption(data []string, boundaryGap ...*bool) XAxisOption { + opt := XAxisOption{ + Data: data, + } + if len(boundaryGap) != 0 { + opt.BoundaryGap = boundaryGap[0] + } + return opt +} + +func (opt *XAxisOption) ToAxisPainterOption() AxisPainterOption { + position := PositionBottom + if opt.Position == PositionTop { + position = PositionTop + } + return AxisPainterOption{ + Theme: opt.Theme, + Data: opt.Data, + BoundaryGap: opt.BoundaryGap, + Position: position, + SplitNumber: opt.SplitNumber, + StrokeColor: opt.StrokeColor, + FontSize: opt.FontSize, + Font: opt.Font, + FontColor: opt.FontColor, + } +} + +func NewBottomXAxis(p *Painter, opt XAxisOption) *axisPainter { + p = p.Child(PainterPaddingOption(Box{ + Top: p.Height() - defaultXAxisHeight, + })) + return NewAxisPainter(p, opt.ToAxisPainterOption()) +} diff --git a/yaxis.go b/yaxis.go new file mode 100644 index 0000000..653f6ec --- /dev/null +++ b/yaxis.go @@ -0,0 +1,66 @@ +// 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 YAxisOption struct { + // The font of y axis + Font *truetype.Font + // The data value of x axis + Data []string + // The theme of chart + Theme ColorPalette + // The font size of x axis label + FontSize float64 + // The position of axis, it can be 'left' or 'right' + Position string + // The color of label + FontColor Color +} + +func (opt *YAxisOption) ToAxisPainterOption() AxisPainterOption { + position := PositionLeft + if opt.Position == PositionRight { + position = PositionRight + } + return AxisPainterOption{ + Theme: opt.Theme, + Data: opt.Data, + Position: position, + FontSize: opt.FontSize, + StrokeWidth: -1, + Font: opt.Font, + FontColor: opt.FontColor, + BoundaryGap: FalseFlag(), + SplitLineShow: true, + SplitLineColor: opt.Theme.GetAxisSplitLineColor(), + } +} + +func NewLeftYAxis(p *Painter, opt YAxisOption) *axisPainter { + p = p.Child(PainterPaddingOption(Box{ + Bottom: defaultXAxisHeight, + })) + return NewAxisPainter(p, opt.ToAxisPainterOption()) +} From 72e11e49b17a39e897e5e6dd3bb88aabf54c29e4 Mon Sep 17 00:00:00 2001 From: vicanso Date: Sun, 12 Jun 2022 19:58:36 +0800 Subject: [PATCH 014/116] refactor: default render function for axis --- charts.go | 84 ++++++++++++++++++++++++++++++++++++++++++++++++--- line_chart.go | 53 +++++++------------------------- series.go | 9 ++++-- 3 files changed, 97 insertions(+), 49 deletions(-) diff --git a/charts.go b/charts.go index 591ebea..6a2f5f2 100644 --- a/charts.go +++ b/charts.go @@ -27,16 +27,92 @@ type Renderer interface { } type defaultRenderOption struct { - Theme ColorPalette - Padding Box + Theme ColorPalette + Padding Box + SeriesList SeriesList + // The y axis option + YAxisOptions []YAxisOption + // The x axis option + XAxis XAxisOption } -func defaultRender(p *Painter, opt defaultRenderOption) *Painter { +type defaultRenderResult struct { + axisRanges map[int]axisRange + p *Painter +} + +func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, error) { p.SetBackground(p.Width(), p.Height(), opt.Theme.GetBackgroundColor()) if !opt.Padding.IsZero() { p = p.Child(PainterPaddingOption(opt.Padding)) } - return p + result := defaultRenderResult{ + axisRanges: make(map[int]axisRange), + } + + // 计算图表对应的轴有哪些 + axisIndexList := make([]int, 0) + for _, series := range opt.SeriesList { + if containsInt(axisIndexList, series.AxisIndex) { + continue + } + axisIndexList = append(axisIndexList, series.index) + } + // 高度需要减去x轴的高度 + rangeHeight := p.Height() - defaultXAxisHeight + rangeWidth := 0 + + // 计算对应的axis range + for _, index := range axisIndexList { + max, min := opt.SeriesList.GetMaxMin(index) + r := NewRange(AxisRangeOption{ + Min: min, + Max: max, + // 高度需要减去x轴的高度 + Size: rangeHeight, + // 分隔数量 + DivideCount: defaultAxisDivideCount, + }) + result.axisRanges[index] = r + yAxisOption := YAxisOption{} + if len(opt.YAxisOptions) > index { + yAxisOption = opt.YAxisOptions[index] + } + if yAxisOption.Theme == nil { + yAxisOption.Theme = opt.Theme + } + yAxisOption.Data = r.Values() + reverseStringSlice(yAxisOption.Data) + // TODO生成其它位置既yAxis + yAxis := NewLeftYAxis(p, yAxisOption) + yAxisBox, err := yAxis.Render() + if err != nil { + return nil, err + } + rangeWidth += yAxisBox.Width() + } + + if opt.XAxis.Theme == nil { + opt.XAxis.Theme = opt.Theme + } + xAxis := NewBottomXAxis(p.Child(PainterPaddingOption(Box{ + Left: rangeWidth, + })), opt.XAxis) + _, err := xAxis.Render() + if err != nil { + return nil, err + } + + // // 生成Y轴 + // for _, yAxisOption := range opt.YAxisOptions { + + // } + + result.p = p.Child(PainterPaddingOption(Box{ + Bottom: rangeHeight, + Left: rangeWidth, + })) + return &result, nil } func doRender(renderers ...Renderer) error { diff --git a/line_chart.go b/line_chart.go index 9640087..3d93341 100644 --- a/line_chart.go +++ b/line_chart.go @@ -50,53 +50,28 @@ type LineChartOption struct { // The padding of line chart Padding Box // The y axis option - YAxis YAxisOption + YAxisOptions []YAxisOption } func (l *lineChart) Render() (Box, error) { p := l.p opt := l.opt - p = defaultRender(p, defaultRenderOption{ - Theme: opt.Theme, - Padding: opt.Padding, - }) - seriesList := opt.SeriesList seriesList.init() - // 过滤前先计算最大最小值 - max, min := seriesList.GetMaxMin() - - seriesList = seriesList.Filter(ChartTypeLine) - - // Y轴 - yr := NewRange(AxisRangeOption{ - Min: min, - Max: max, - // 高度需要减去x轴的高度 - Size: p.Height() - defaultXAxisHeight, - DivideCount: defaultAxisDivideCount, + renderResult, err := defaultRender(p, defaultRenderOption{ + Theme: opt.Theme, + Padding: opt.Padding, + SeriesList: seriesList, + XAxis: opt.XAxis, + YAxisOptions: opt.YAxisOptions, }) - if opt.YAxis.Theme == nil { - opt.YAxis.Theme = opt.Theme - } - opt.YAxis.Data = yr.Values() - reverseStringSlice(opt.YAxis.Data) - yAxis := NewLeftYAxis(p, opt.YAxis) - yAxisBox, err := yAxis.Render() if err != nil { return chart.BoxZero, err } - seriesPainter := p.Child(PainterPaddingOption(Box{ - Bottom: defaultXAxisHeight, - Left: yAxisBox.Width(), - })) - if opt.XAxis.Theme == nil { - opt.XAxis.Theme = opt.Theme - } - xAxis := NewBottomXAxis(p.Child(PainterPaddingOption(Box{ - Left: yAxisBox.Width(), - })), opt.XAxis) + seriesList = seriesList.Filter(ChartTypeLine) + + seriesPainter := renderResult.p xDivideValues := autoDivide(seriesPainter.Width(), len(opt.XAxis.Data)) xValues := make([]int, len(xDivideValues)-1) @@ -110,6 +85,7 @@ func (l *lineChart) Render() (Box, error) { StrokeWidth: 2, FillColor: seriesColor, }) + yr := renderResult.axisRanges[series.AxisIndex] points := make([]Point, 0) for i, item := range series.Data { h := yr.getRestHeight(item.Value) @@ -123,12 +99,5 @@ func (l *lineChart) Render() (Box, error) { seriesPainter.Dots(points) } - err = doRender( - xAxis, - ) - if err != nil { - return chart.BoxZero, err - } - return p.box, nil } diff --git a/series.go b/series.go index 07bd5d9..2888f30 100644 --- a/series.go +++ b/series.go @@ -99,8 +99,8 @@ type Series struct { // The data list of series Data []SeriesData // The Y axis index, it should be 0 or 1. - // Default value is 1 - YAxisIndex int + // Default value is 0 + AxisIndex int // The style for series Style chart.Style // The label for series @@ -143,10 +143,13 @@ func (sl SeriesList) Filter(chartType string) SeriesList { } // GetMaxMin get max and min value of series list -func (sl SeriesList) GetMaxMin() (float64, float64) { +func (sl SeriesList) GetMaxMin(axisIndex int) (float64, float64) { min := math.MaxFloat64 max := -math.MaxFloat64 for _, series := range sl { + if series.AxisIndex != axisIndex { + continue + } for _, item := range series.Data { if item.Value > max { max = item.Value From 8a5990fe8fd8ce0f3b40538ed645840c3e587f47 Mon Sep 17 00:00:00 2001 From: vicanso Date: Mon, 13 Jun 2022 23:22:15 +0800 Subject: [PATCH 015/116] feat: support mark line and mark point render --- chart_option.go | 120 ++++ charts.go | 117 +++- examples/charts/main.go | 1321 +++++++++++++++++++++++++++++++++++ examples/line_chart/main.go | 13 + legend.go | 24 +- line_chart.go | 56 +- mark_line.go | 118 ++++ mark_point.go | 102 +++ painter.go | 2 +- title.go | 192 +++++ 10 files changed, 2046 insertions(+), 19 deletions(-) create mode 100644 chart_option.go create mode 100644 examples/charts/main.go create mode 100644 mark_line.go create mode 100644 mark_point.go create mode 100644 title.go diff --git a/chart_option.go b/chart_option.go new file mode 100644 index 0000000..6ca7cd7 --- /dev/null +++ b/chart_option.go @@ -0,0 +1,120 @@ +// 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 ( + "sort" + + "github.com/golang/freetype/truetype" + "github.com/wcharczuk/go-chart/v2" +) + +type ChartOption struct { + theme ColorPalette + font *truetype.Font + // The output type of chart, "svg" or "png", default value is "svg" + Type string + // The font family, which should be installed first + FontFamily string + // The theme of chart, "light" and "dark". + // The default theme is "light" + Theme string + // The title option + Title TitleOption + // The legend option + Legend LegendOption + // The x axis option + XAxis XAxisOption + // The y axis option list + YAxisOptions []YAxisOption + // The width of chart, default width is 600 + Width int + // The height of chart, default height is 400 + Height int + Parent *Painter + // The padding for chart, default padding is [20, 10, 10, 10] + Padding Box + // The canvas box for chart + Box Box + // The series list + SeriesList SeriesList + // The radar indicator list + // RadarIndicators []RadarIndicator + // The background color of chart + BackgroundColor Color + // The child charts + Children []ChartOption +} + +func (o *ChartOption) fillDefault() { + t := NewTheme(o.Theme) + o.theme = t + // 如果为空,初始化 + axisCount := 1 + for _, series := range o.SeriesList { + if series.AxisIndex >= axisCount { + axisCount++ + } + } + o.Width = getDefaultInt(o.Width, defaultChartWidth) + o.Height = getDefaultInt(o.Height, defaultChartHeight) + yAxisOptions := make([]YAxisOption, axisCount) + copy(yAxisOptions, o.YAxisOptions) + o.YAxisOptions = yAxisOptions + o.font, _ = GetFont(o.FontFamily) + + if o.font == nil { + o.font, _ = chart.GetDefaultFont() + } + if o.BackgroundColor.IsZero() { + o.BackgroundColor = t.GetBackgroundColor() + } + if o.Padding.IsZero() { + o.Padding = chart.Box{ + Top: 10, + Right: 10, + Bottom: 10, + Left: 10, + } + } + // legend与series name的关联 + if len(o.Legend.Data) == 0 { + o.Legend.Data = o.SeriesList.Names() + } else { + seriesCount := len(o.SeriesList) + for index, name := range o.Legend.Data { + if index < seriesCount && + len(o.SeriesList[index].Name) == 0 { + o.SeriesList[index].Name = name + } + } + nameIndexDict := map[string]int{} + for index, name := range o.Legend.Data { + nameIndexDict[name] = index + } + // 保证series的顺序与legend一致 + sort.Slice(o.SeriesList, func(i, j int) bool { + return nameIndexDict[o.SeriesList[i].Name] < nameIndexDict[o.SeriesList[j].Name] + }) + } +} diff --git a/charts.go b/charts.go index 6a2f5f2..947fa8d 100644 --- a/charts.go +++ b/charts.go @@ -22,6 +22,24 @@ package charts +const labelFontSize = 10 +const defaultDotWidth = 2.0 +const defaultStrokeWidth = 2.0 + +var defaultChartWidth = 600 +var defaultChartHeight = 400 + +func SetDefaultWidth(width int) { + if width > 0 { + defaultChartWidth = width + } +} +func SetDefaultHeight(height int) { + if height > 0 { + defaultChartHeight = height + } +} + type Renderer interface { Render() (Box, error) } @@ -34,6 +52,10 @@ type defaultRenderOption struct { YAxisOptions []YAxisOption // The x axis option XAxis XAxisOption + // The title option + TitleOption TitleOption + // The legend option + LegendOption LegendOption } type defaultRenderResult struct { @@ -42,10 +64,37 @@ type defaultRenderResult struct { } func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, error) { - p.SetBackground(p.Width(), p.Height(), opt.Theme.GetBackgroundColor()) if !opt.Padding.IsZero() { p = p.Child(PainterPaddingOption(opt.Padding)) } + + if len(opt.LegendOption.Data) != 0 { + if opt.LegendOption.Theme == nil { + opt.LegendOption.Theme = opt.Theme + } + _, err := NewLegendPainter(p, opt.LegendOption).Render() + if err != nil { + return nil, err + } + } + + // 如果有标题 + if opt.TitleOption.Text != "" { + if opt.TitleOption.Theme == nil { + opt.TitleOption.Theme = opt.Theme + } + titlePainter := NewTitlePainter(p, opt.TitleOption) + + titleBox, err := titlePainter.Render() + if err != nil { + return nil, err + } + p = p.Child(PainterPaddingOption(Box{ + // 标题下留白 + Top: titleBox.Height() + 20, + })) + } + result := defaultRenderResult{ axisRanges: make(map[int]axisRange), } @@ -60,7 +109,8 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e } // 高度需要减去x轴的高度 rangeHeight := p.Height() - defaultXAxisHeight - rangeWidth := 0 + rangeWidthLeft := 0 + rangeWidthRight := 0 // 计算对应的axis range for _, index := range axisIndexList { @@ -89,28 +139,28 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e if err != nil { return nil, err } - rangeWidth += yAxisBox.Width() + if index == 0 { + rangeWidthLeft += yAxisBox.Width() + } else { + rangeWidthRight += yAxisBox.Width() + } } if opt.XAxis.Theme == nil { opt.XAxis.Theme = opt.Theme } xAxis := NewBottomXAxis(p.Child(PainterPaddingOption(Box{ - Left: rangeWidth, + Left: rangeWidthLeft, })), opt.XAxis) _, err := xAxis.Render() if err != nil { return nil, err } - // // 生成Y轴 - // for _, yAxisOption := range opt.YAxisOptions { - - // } - result.p = p.Child(PainterPaddingOption(Box{ Bottom: rangeHeight, - Left: rangeWidth, + Left: rangeWidthLeft, + Right: rangeWidthRight, })) return &result, nil } @@ -124,3 +174,50 @@ func doRender(renderers ...Renderer) error { } return nil } + +func Render(opt ChartOption) (*Painter, error) { + opt.fillDefault() + + if opt.Parent == nil { + p, err := NewPainter(PainterOptions{ + Type: opt.Type, + Width: opt.Width, + Height: opt.Height, + }) + if err != nil { + return nil, err + } + opt.Parent = p + } + p := opt.Parent + p.SetBackground(p.Width(), p.Height(), opt.BackgroundColor) + seriesList := opt.SeriesList + seriesList.init() + + rendererList := make([]Renderer, 0) + + // line chart + lineChartSeriesList := seriesList.Filter(ChartTypeLine) + if len(lineChartSeriesList) != 0 { + renderer := NewLineChart(p, LineChartOption{ + Theme: opt.theme, + Font: opt.font, + SeriesList: lineChartSeriesList, + XAxis: opt.XAxis, + Padding: opt.Padding, + YAxisOptions: opt.YAxisOptions, + TitleOption: opt.Title, + LegendOption: opt.Legend, + }) + rendererList = append(rendererList, renderer) + } + + for _, renderer := range rendererList { + _, err := renderer.Render() + if err != nil { + return nil, err + } + } + + return p, nil +} diff --git a/examples/charts/main.go b/examples/charts/main.go new file mode 100644 index 0000000..18f5a95 --- /dev/null +++ b/examples/charts/main.go @@ -0,0 +1,1321 @@ +package main + +import ( + "bytes" + "net/http" + "strconv" + + charts "github.com/vicanso/go-charts" +) + +var html = ` + + + + + + + go-charts + + +
{{body}}
+ + +` + +func handler(w http.ResponseWriter, req *http.Request, chartOptions []charts.ChartOption, echartsOptions []string) { + if req.URL.Path != "/" && + req.URL.Path != "/echarts" { + return + } + query := req.URL.Query() + theme := query.Get("theme") + width, _ := strconv.Atoi(query.Get("width")) + height, _ := strconv.Atoi(query.Get("height")) + charts.SetDefaultWidth(width) + charts.SetDefaultWidth(height) + bytesList := make([][]byte, 0) + for _, opt := range chartOptions { + opt.Theme = theme + d, err := charts.Render(opt) + if err != nil { + panic(err) + } + buf, err := d.Bytes() + if err != nil { + panic(err) + } + bytesList = append(bytesList, buf) + } + // for _, opt := range echartsOptions { + // buf, err := charts.RenderEChartsToSVG(opt) + // if err != nil { + // panic(err) + // } + // bytesList = append(bytesList, buf) + // } + + data := bytes.ReplaceAll([]byte(html), []byte("{{body}}"), bytes.Join(bytesList, []byte(""))) + w.Header().Set("Content-Type", "text/html") + w.Write(data) +} + +func indexHandler(w http.ResponseWriter, req *http.Request) { + chartOptions := []charts.ChartOption{ + { + Title: charts.TitleOption{ + Text: "Line", + }, + Legend: charts.NewLegendOption([]string{ + "Email", + "Union Ads", + "Video Ads", + "Direct", + "Search Engine", + }), + XAxis: charts.NewXAxisOption([]string{ + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun", + }), + SeriesList: []charts.Series{ + charts.NewSeriesFromValues([]float64{ + 120, + 132, + 101, + 134, + 90, + 230, + 210, + }), + charts.NewSeriesFromValues([]float64{ + 220, + 182, + 191, + 234, + 290, + 330, + 310, + }), + charts.NewSeriesFromValues([]float64{ + 150, + 232, + 201, + 154, + 190, + 330, + 410, + }), + charts.NewSeriesFromValues([]float64{ + 320, + 332, + 301, + 334, + 390, + 330, + 320, + }), + charts.NewSeriesFromValues([]float64{ + 820, + 932, + 901, + 934, + 1290, + 1330, + 1320, + }), + }, + }, + // 温度折线图 + { + Title: charts.TitleOption{ + Text: "Temperature Change in the Coming Week", + }, + Padding: charts.Box{ + Top: 20, + Left: 20, + Right: 30, + Bottom: 20, + }, + Legend: charts.NewLegendOption([]string{ + "Highest", + "Lowest", + }, charts.PositionRight), + XAxis: charts.NewXAxisOption([]string{ + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun", + }, charts.FalseFlag()), + SeriesList: []charts.Series{ + { + Data: charts.NewSeriesDataFromValues([]float64{ + 14, + 11, + 13, + 11, + 12, + 12, + 7, + }), + MarkPoint: charts.NewMarkPoint(charts.SeriesMarkDataTypeMax, charts.SeriesMarkDataTypeMin), + MarkLine: charts.NewMarkLine(charts.SeriesMarkDataTypeAverage), + }, + { + Data: charts.NewSeriesDataFromValues([]float64{ + 1, + -2, + 2, + 5, + 3, + 2, + 0, + }), + MarkLine: charts.NewMarkLine(charts.SeriesMarkDataTypeAverage), + }, + }, + }, + } + handler(w, req, chartOptions, nil) +} + +func echartsHandler(w http.ResponseWriter, req *http.Request) { + echartsOptions := []string{ + `{ + "xAxis": { + "type": "category", + "data": [ + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun" + ] + }, + "yAxis": { + "type": "value" + }, + "series": [ + { + "data": [ + 150, + 230, + 224, + 218, + 135, + 147, + 260 + ], + "type": "line" + } + ] + }`, + `{ + "title": { + "text": "Multiple Line" + }, + "tooltip": { + "trigger": "axis" + }, + "legend": { + "left": "right", + "data": [ + "Email", + "Union Ads", + "Video Ads", + "Direct", + "Search Engine" + ] + }, + "grid": { + "left": "3%", + "right": "4%", + "bottom": "3%", + "containLabel": true + }, + "toolbox": { + "feature": { + "saveAsImage": {} + } + }, + "xAxis": { + "type": "category", + "boundaryGap": false, + "data": [ + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun" + ] + }, + "yAxis": { + "type": "value" + }, + "series": [ + { + "name": "Email", + "type": "line", + "data": [ + 120, + 132, + 101, + 134, + 90, + 230, + 210 + ] + }, + { + "name": "Union Ads", + "type": "line", + "data": [ + 220, + 182, + 191, + 234, + 290, + 330, + 310 + ] + }, + { + "name": "Video Ads", + "type": "line", + "data": [ + 150, + 232, + 201, + 154, + 190, + 330, + 410 + ] + }, + { + "name": "Direct", + "type": "line", + "data": [ + 320, + 332, + 301, + 334, + 390, + 330, + 320 + ] + }, + { + "name": "Search Engine", + "type": "line", + "data": [ + 820, + 932, + 901, + 934, + 1290, + 1330, + 1320 + ] + } + ] + }`, + `{ + "title": { + "text": "Temperature Change in the Coming Week" + }, + "legend": { + "left": "right" + }, + "padding": [10, 30, 10, 10], + "xAxis": { + "type": "category", + "boundaryGap": false, + "data": [ + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun" + ] + }, + "yAxis": { + "axisLabel": { + "formatter": "{value} °C" + } + }, + "series": [ + { + "name": "Highest", + "type": "line", + "data": [ + 10, + 11, + 13, + 11, + 12, + 12, + 9 + ], + "markPoint": { + "data": [ + { + "type": "max" + }, + { + "type": "min" + } + ] + }, + "markLine": { + "data": [ + { + "type": "average" + } + ] + } + }, + { + "name": "Lowest", + "type": "line", + "data": [ + 1, + -2, + 2, + 5, + 3, + 2, + 0 + ], + "markPoint": { + "data": [ + { + "type": "min" + } + ] + }, + "markLine": { + "data": [ + { + "type": "average" + }, + { + "type": "max" + } + ] + } + } + ] + }`, + `{ + "xAxis": { + "type": "category", + "data": [ + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun" + ] + }, + "yAxis": { + "type": "value" + }, + "series": [ + { + "data": [ + 120, + 200, + 150, + 80, + 70, + 110, + 130 + ], + "type": "bar" + } + ] + }`, + `{ + "xAxis": { + "type": "category", + "data": [ + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun" + ] + }, + "yAxis": { + "type": "value" + }, + "series": [ + { + "data": [ + 120, + { + "value": 200, + "itemStyle": { + "color": "#a90000" + } + }, + 150, + 80, + 70, + 110, + 130 + ], + "type": "bar" + } + ] + }`, + `{ + "title": { + "text": "Rainfall vs Evaporation", + "subtext": "Fake Data" + }, + "legend": { + "data": [ + "Rainfall", + "Evaporation" + ] + }, + "padding": [10, 30, 10, 10], + "xAxis": [ + { + "type": "category", + "data": [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec" + ] + } + ], + "series": [ + { + "name": "Rainfall", + "type": "bar", + "data": [ + 2, + 4.9, + 7, + 23.2, + 25.6, + 76.7, + 135.6, + 162.2, + 32.6, + 20, + 6.4, + 3.3 + ], + "markPoint": { + "data": [ + { + "type": "max" + }, + { + "type": "min" + } + ] + }, + "markLine": { + "data": [ + { + "type": "average" + } + ] + } + }, + { + "name": "Evaporation", + "type": "bar", + "data": [ + 2.6, + 5.9, + 9, + 26.4, + 28.7, + 70.7, + 175.6, + 182.2, + 48.7, + 18.8, + 6, + 2.3 + ], + "markPoint": { + "data": [ + { + "type": "max" + }, + { + "type": "min" + } + ] + }, + "markLine": { + "data": [ + { + "type": "average" + } + ] + } + } + ] + }`, + `{ + "legend": { + "data": [ + "Evaporation", + "Precipitation", + "Temperature" + ] + }, + "xAxis": [ + { + "type": "category", + "data": [ + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun" + ], + "axisPointer": { + "type": "shadow" + } + } + ], + "yAxis": [ + { + "type": "value", + "name": "Precipitation", + "min": 0, + "max": 240, + "axisLabel": { + "formatter": "{value} ml" + } + }, + { + "type": "value", + "name": "Temperature", + "min": 0, + "max": 24, + "axisLabel": { + "formatter": "{value} °C" + } + } + ], + "series": [ + { + "name": "Evaporation", + "type": "bar", + "tooltip": {}, + "data": [ + 2, + 4.9, + 7, + 23.2, + 25.6, + 76.7, + 135.6, + 162.2, + 32.6, + 20, + 6.4, + 3.3 + ] + }, + { + "name": "Precipitation", + "type": "bar", + "tooltip": {}, + "data": [ + 2.6, + 5.9, + 9, + 26.4, + 28.7, + 70.7, + 175.6, + 182.2, + 48.7, + 18.8, + 6, + 2.3 + ] + }, + { + "name": "Temperature", + "type": "line", + "yAxisIndex": 1, + "tooltip": {}, + "data": [ + 2, + 2.2, + 3.3, + 4.5, + 6.3, + 10.2, + 20.3, + 23.4, + 23, + 16.5, + 12, + 6.2 + ] + } + ] + }`, + `{ + "tooltip": { + "trigger": "axis", + "axisPointer": { + "type": "cross" + } + }, + "grid": { + "right": "20%" + }, + "toolbox": { + "feature": { + "dataView": { + "show": true, + "readOnly": false + }, + "restore": { + "show": true + }, + "saveAsImage": { + "show": true + } + } + }, + "legend": { + "data": [ + "Evaporation", + "Precipitation", + "Temperature" + ] + }, + "xAxis": [ + { + "type": "category", + "axisTick": { + "alignWithLabel": true + }, + "data": [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec" + ] + } + ], + "yAxis": [ + { + "type": "value", + "name": "温度", + "position": "left", + "alignTicks": true, + "axisLine": { + "show": true, + "lineStyle": { + "color": "#EE6666" + } + }, + "axisLabel": { + "formatter": "{value} °C" + } + }, + { + "type": "value", + "name": "Evaporation", + "position": "right", + "alignTicks": true, + "axisLine": { + "show": true, + "lineStyle": { + "color": "#5470C6" + } + }, + "axisLabel": { + "formatter": "{value} ml" + } + } + ], + "series": [ + { + "name": "Evaporation", + "type": "bar", + "yAxisIndex": 1, + "data": [ + 2, + 4.9, + 7, + 23.2, + 25.6, + 76.7, + 135.6, + 162.2, + 32.6, + 20, + 6.4, + 3.3 + ] + }, + { + "name": "Precipitation", + "type": "bar", + "yAxisIndex": 1, + "data": [ + 2.6, + 5.9, + 9, + 26.4, + 28.7, + 70.7, + 175.6, + 182.2, + 48.7, + 18.8, + 6, + 2.3 + ] + }, + { + "name": "Temperature", + "type": "line", + "data": [ + 2, + 2.2, + 3.3, + 4.5, + 6.3, + 10.2, + 20.3, + 23.4, + 23, + 16.5, + 12, + 6.2 + ] + } + ] + }`, + `{ + "title": { + "text": "Referer of a Website", + "subtext": "Fake Data", + "left": "center" + }, + "tooltip": { + "trigger": "item" + }, + "legend": { + "orient": "vertical", + "left": "left" + }, + "series": [ + { + "name": "Access From", + "type": "pie", + "radius": "50%", + "data": [ + { + "value": 1048, + "name": "Search Engine" + }, + { + "value": 735, + "name": "Direct" + }, + { + "value": 580, + "name": "Email" + }, + { + "value": 484, + "name": "Union Ads" + }, + { + "value": 300, + "name": "Video Ads" + } + ] + } + ] + }`, + `{ + "title": { + "text": "Rainfall" + }, + "padding": [10, 10, 10, 30], + "legend": { + "data": [ + "GZ", + "SH" + ] + }, + "xAxis": { + "type": "category", + "splitNumber": 6, + "data": [ + "01-01", + "01-02", + "01-03", + "01-04", + "01-05", + "01-06", + "01-07", + "01-08", + "01-09", + "01-10", + "01-11", + "01-12", + "01-13", + "01-14", + "01-15", + "01-16", + "01-17", + "01-18", + "01-19", + "01-20", + "01-21", + "01-22", + "01-23", + "01-24", + "01-25", + "01-26", + "01-27", + "01-28", + "01-29", + "01-30", + "01-31" + ] + }, + "yAxis": { + "axisLabel": { + "formatter": "{value} mm" + } + }, + "series": [ + { + "type": "bar", + "data": [ + 928, + 821, + 889, + 600, + 547, + 783, + 197, + 853, + 430, + 346, + 63, + 465, + 309, + 334, + 141, + 538, + 792, + 58, + 922, + 807, + 298, + 243, + 744, + 885, + 812, + 231, + 330, + 220, + 984, + 221, + 429 + ] + }, + { + "type": "bar", + "data": [ + 749, + 201, + 296, + 579, + 255, + 159, + 902, + 246, + 149, + 158, + 507, + 776, + 186, + 79, + 390, + 222, + 601, + 367, + 221, + 411, + 714, + 620, + 966, + 73, + 203, + 631, + 833, + 610, + 487, + 677, + 596 + ] + } + ] + }`, + `{ + "title": { + "text": "Basic Radar Chart" + }, + "legend": { + "data": [ + "Allocated Budget", + "Actual Spending" + ] + }, + "radar": { + "indicator": [ + { + "name": "Sales", + "max": 6500 + }, + { + "name": "Administration", + "max": 16000 + }, + { + "name": "Information Technology", + "max": 30000 + }, + { + "name": "Customer Support", + "max": 38000 + }, + { + "name": "Development", + "max": 52000 + }, + { + "name": "Marketing", + "max": 25000 + } + ] + }, + "series": [ + { + "name": "Budget vs spending", + "type": "radar", + "data": [ + { + "value": [ + 4200, + 3000, + 20000, + 35000, + 50000, + 18000 + ], + "name": "Allocated Budget" + }, + { + "value": [ + 5000, + 14000, + 28000, + 26000, + 42000, + 21000 + ], + "name": "Actual Spending" + } + ] + } + ] + }`, + `{ + "title": { + "text": "Funnel" + }, + "tooltip": { + "trigger": "item", + "formatter": "{a}
{b} : {c}%" + }, + "toolbox": { + "feature": { + "dataView": { + "readOnly": false + }, + "restore": {}, + "saveAsImage": {} + } + }, + "legend": { + "data": [ + "Show", + "Click", + "Visit", + "Inquiry", + "Order" + ] + }, + "series": [ + { + "name": "Funnel", + "type": "funnel", + "left": "10%", + "top": 60, + "bottom": 60, + "width": "80%", + "min": 0, + "max": 100, + "minSize": "0%", + "maxSize": "100%", + "sort": "descending", + "gap": 2, + "label": { + "show": true, + "position": "inside" + }, + "labelLine": { + "length": 10, + "lineStyle": { + "width": 1, + "type": "solid" + } + }, + "itemStyle": { + "borderColor": "#fff", + "borderWidth": 1 + }, + "emphasis": { + "label": { + "fontSize": 20 + } + }, + "data": [ + { + "value": 60, + "name": "Visit" + }, + { + "value": 40, + "name": "Inquiry" + }, + { + "value": 20, + "name": "Order" + }, + { + "value": 80, + "name": "Click" + }, + { + "value": 100, + "name": "Show" + } + ] + } + ] + }`, + `{ + "legend": { + "top": "-140", + "data": [ + "Milk Tea", + "Matcha Latte", + "Cheese Cocoa", + "Walnut Brownie" + ] + }, + "padding": [ + 150, + 10, + 10, + 10 + ], + "xAxis": [ + { + "data": [ + "2012", + "2013", + "2014", + "2015", + "2016", + "2017" + ] + } + ], + "series": [ + { + "data": [ + 56.5, + 82.1, + 88.7, + 70.1, + 53.4, + 85.1 + ] + }, + { + "data": [ + 51.1, + 51.4, + 55.1, + 53.3, + 73.8, + 68.7 + ] + }, + { + "data": [ + 40.1, + 62.2, + 69.5, + 36.4, + 45.2, + 32.5 + ] + }, + { + "data": [ + 25.2, + 37.1, + 41.2, + 18, + 33.9, + 49.1 + ] + } + ], + "children": [ + { + "box": { + "left": 0, + "top": 30, + "right": 600, + "bottom": 150 + }, + "legend": { + "show": false + }, + "series": [ + { + "type": "pie", + "radius": "50%", + "data": [ + { + "value": 435.9, + "name": "Milk Tea" + }, + { + "value": 354.3, + "name": "Matcha Latte" + }, + { + "value": 285.9, + "name": "Cheese Cocoa" + }, + { + "value": 204.5, + "name": "Walnut Brownie" + } + ] + } + ] + } + ] + }`, + } + handler(w, req, nil, echartsOptions) +} + +func main() { + http.HandleFunc("/", indexHandler) + http.HandleFunc("/echarts", echartsHandler) + http.ListenAndServe(":3012", nil) +} diff --git a/examples/line_chart/main.go b/examples/line_chart/main.go index e15500c..c168f08 100644 --- a/examples/line_chart/main.go +++ b/examples/line_chart/main.go @@ -39,6 +39,19 @@ func main() { Right: 10, Bottom: 10, }, + TitleOption: charts.TitleOption{ + Text: "Line", + }, + LegendOption: charts.LegendOption{ + Data: []string{ + "Email", + "Union Ads", + "Video Ads", + "Direct", + "Search Engine", + }, + Left: charts.PositionCenter, + }, XAxis: charts.NewXAxisOption([]string{ "Mon", "Tue", diff --git a/legend.go b/legend.go index 16341e4..b8a6fdc 100644 --- a/legend.go +++ b/legend.go @@ -29,13 +29,13 @@ import ( type legendPainter struct { p *Painter - opt *LegendPainterOption + opt *LegendOption } const IconRect = "rect" const IconLineDot = "lineDot" -type LegendPainterOption struct { +type LegendOption struct { Theme ColorPalette // Text array of legend Data []string @@ -58,7 +58,17 @@ type LegendPainterOption struct { FontColor Color } -func NewLegendPainter(p *Painter, opt LegendPainterOption) *legendPainter { +func NewLegendOption(labels []string, left ...string) LegendOption { + opt := LegendOption{ + Data: labels, + } + if len(left) != 0 { + opt.Left = left[0] + } + return opt +} + +func NewLegendPainter(p *Painter, opt LegendOption) *legendPainter { return &legendPainter{ p: p, opt: &opt, @@ -71,6 +81,12 @@ func (l *legendPainter) Render() (Box, error) { if theme == nil { theme = l.p.theme } + if opt.FontSize == 0 { + opt.FontSize = theme.GetFontSize() + } + if opt.FontColor.IsZero() { + opt.FontColor = theme.GetTextColor() + } p := l.p p.SetTextStyle(Style{ FontSize: opt.FontSize, @@ -129,7 +145,7 @@ func (l *legendPainter) Render() (Box, error) { top, _ := strconv.Atoi(opt.Top) x := int(left) - y := int(top) + y := int(top) + 10 x0 := x y0 := y diff --git a/line_chart.go b/line_chart.go index 3d93341..5f4ea4f 100644 --- a/line_chart.go +++ b/line_chart.go @@ -23,7 +23,9 @@ package charts import ( + "github.com/golang/freetype/truetype" "github.com/wcharczuk/go-chart/v2" + "github.com/wcharczuk/go-chart/v2/drawing" ) type lineChart struct { @@ -43,6 +45,8 @@ func NewLineChart(p *Painter, opt LineChartOption) *lineChart { type LineChartOption struct { Theme ColorPalette + // The font size + Font *truetype.Font // The data series list SeriesList SeriesList // The x axis option @@ -51,6 +55,10 @@ type LineChartOption struct { Padding Box // The y axis option YAxisOptions []YAxisOption + // The option of title + TitleOption TitleOption + // The legend option + LegendOption LegendOption } func (l *lineChart) Render() (Box, error) { @@ -64,6 +72,8 @@ func (l *lineChart) Render() (Box, error) { SeriesList: seriesList, XAxis: opt.XAxis, YAxisOptions: opt.YAxisOptions, + TitleOption: opt.TitleOption, + LegendOption: opt.LegendOption, }) if err != nil { return chart.BoxZero, err @@ -78,13 +88,20 @@ func (l *lineChart) Render() (Box, error) { for i := 0; i < len(xDivideValues)-1; i++ { xValues[i] = (xDivideValues[i] + xDivideValues[i+1]) >> 1 } + markPointPainter := NewMarkPointPainter(seriesPainter) + markLinePainter := NewMarkLinePainter(seriesPainter) + rendererList := []Renderer{ + markPointPainter, + markLinePainter, + } for index, series := range seriesList { seriesColor := opt.Theme.GetSeriesColor(index) - seriesPainter.SetDrawingStyle(Style{ + drawingStyle := Style{ StrokeColor: seriesColor, - StrokeWidth: 2, - FillColor: seriesColor, - }) + StrokeWidth: defaultStrokeWidth, + } + + seriesPainter.SetDrawingStyle(drawingStyle) yr := renderResult.axisRanges[series.AxisIndex] points := make([]Point, 0) for i, item := range series.Data { @@ -95,8 +112,39 @@ func (l *lineChart) Render() (Box, error) { } points = append(points, p) } + // 画线 seriesPainter.LineStroke(points) + + // 画点 + if opt.Theme.IsDark() { + drawingStyle.FillColor = drawingStyle.StrokeColor + } else { + drawingStyle.FillColor = drawing.ColorWhite + } + drawingStyle.StrokeWidth = 1 + seriesPainter.SetDrawingStyle(drawingStyle) seriesPainter.Dots(points) + markPointPainter.Add(markPointRenderOption{ + FillColor: seriesColor, + Font: opt.Font, + Points: points, + Series: series, + }) + markLinePainter.Add(markLineRenderOption{ + FillColor: seriesColor, + FontColor: opt.Theme.GetTextColor(), + StrokeColor: seriesColor, + Font: opt.Font, + Series: series, + Range: yr, + }) + } + // 最大、最小的mark point + for _, renderer := range rendererList { + _, err = renderer.Render() + if err != nil { + return chart.BoxZero, err + } } return p.box, nil diff --git a/mark_line.go b/mark_line.go new file mode 100644 index 0000000..9a9d568 --- /dev/null +++ b/mark_line.go @@ -0,0 +1,118 @@ +// 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" + "github.com/wcharczuk/go-chart/v2" +) + +func NewMarkLine(markLineTypes ...string) SeriesMarkLine { + data := make([]SeriesMarkData, len(markLineTypes)) + for index, t := range markLineTypes { + data[index] = SeriesMarkData{ + Type: t, + } + } + return SeriesMarkLine{ + Data: data, + } +} + +type markLinePainter struct { + p *Painter + options []markLineRenderOption +} + +func (m *markLinePainter) Add(opt markLineRenderOption) { + m.options = append(m.options, opt) +} + +func NewMarkLinePainter(p *Painter) *markLinePainter { + return &markLinePainter{ + p: p, + options: make([]markLineRenderOption, 0), + } +} + +type markLineRenderOption struct { + FillColor Color + FontColor Color + StrokeColor Color + Font *truetype.Font + Series Series + Range axisRange +} + +func (m *markLinePainter) Render() (Box, error) { + painter := m.p + for _, opt := range m.options { + s := opt.Series + if len(s.MarkLine.Data) == 0 { + continue + } + summary := s.Summary() + for _, markLine := range s.MarkLine.Data { + // 由于mark line会修改style,因此每次重新设置 + painter.OverrideDrawingStyle(Style{ + FillColor: opt.FillColor, + StrokeColor: opt.StrokeColor, + StrokeWidth: 1, + StrokeDashArray: []float64{ + 4, + 2, + }, + }).OverrideTextStyle(Style{ + Font: opt.Font, + FontColor: opt.FontColor, + FontSize: labelFontSize, + }) + value := float64(0) + switch markLine.Type { + case SeriesMarkDataTypeMax: + value = summary.MaxValue + case SeriesMarkDataTypeMin: + value = summary.MinValue + default: + value = summary.AverageValue + } + y := opt.Range.getRestHeight(value) + width := painter.Width() + text := commafWithDigits(value) + textBox := painter.MeasureText(text) + painter.MarkLine(0, y, width-2) + painter.Text(text, width, y+textBox.Height()>>1-2) + } + } + return chart.BoxZero, nil +} + +func markLineRender(opt markLineRenderOption) { + // d := opt.Draw + // s := opt.Series + // if len(s.MarkLine.Data) == 0 { + // return + // } + // r := d.Render + +} diff --git a/mark_point.go b/mark_point.go new file mode 100644 index 0000000..ce3cb0f --- /dev/null +++ b/mark_point.go @@ -0,0 +1,102 @@ +// 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" + "github.com/wcharczuk/go-chart/v2" +) + +func NewMarkPoint(markPointTypes ...string) SeriesMarkPoint { + data := make([]SeriesMarkData, len(markPointTypes)) + for index, t := range markPointTypes { + data[index] = SeriesMarkData{ + Type: t, + } + } + return SeriesMarkPoint{ + Data: data, + } +} + +type markPointPainter struct { + p *Painter + options []markPointRenderOption +} + +func (m *markPointPainter) Add(opt markPointRenderOption) { + m.options = append(m.options, opt) +} + +type markPointRenderOption struct { + FillColor Color + Font *truetype.Font + Series Series + Points []Point +} + +func NewMarkPointPainter(p *Painter) *markPointPainter { + return &markPointPainter{ + p: p, + options: make([]markPointRenderOption, 0), + } +} + +func (m *markPointPainter) Render() (Box, error) { + painter := m.p + for _, opt := range m.options { + s := opt.Series + if len(s.MarkPoint.Data) == 0 { + continue + } + points := opt.Points + summary := s.Summary() + symbolSize := s.MarkPoint.SymbolSize + if symbolSize == 0 { + symbolSize = 30 + } + painter.OverrideDrawingStyle(Style{ + FillColor: opt.FillColor, + }).OverrideTextStyle(Style{ + FontColor: NewTheme(ThemeDark).GetTextColor(), + FontSize: labelFontSize, + StrokeWidth: 1, + Font: opt.Font, + }) + for _, markPointData := range s.MarkPoint.Data { + p := points[summary.MinIndex] + value := summary.MinValue + switch markPointData.Type { + case SeriesMarkDataTypeMax: + p = points[summary.MaxIndex] + value = summary.MaxValue + } + + painter.Pin(p.X, p.Y-symbolSize>>1, symbolSize) + text := commafWithDigits(value) + textBox := painter.MeasureText(text) + painter.Text(text, p.X-textBox.Width()>>1, p.Y-symbolSize>>1-2) + } + } + return chart.BoxZero, nil +} diff --git a/painter.go b/painter.go index 75d4a38..fff6ca7 100644 --- a/painter.go +++ b/painter.go @@ -700,7 +700,7 @@ func (p *Painter) Grid(opt GridOption) *Painter { func (p *Painter) Dots(points []Point) *Painter { for _, item := range points { - p.Circle(3, item.X, item.Y) + p.Circle(2, item.X, item.Y) } p.FillStroke() return p diff --git a/title.go b/title.go new file mode 100644 index 0000000..30831ac --- /dev/null +++ b/title.go @@ -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 ( + "strconv" + "strings" + + "github.com/golang/freetype/truetype" +) + +type TitleOption struct { + // The theme of chart + Theme ColorPalette + // Title text, support \n for new line + Text string + // Subtitle text, support \n for new line + Subtext string + // // Title style + // Style Style + // // Subtitle style + // SubtextStyle Style + // Distance between title component and the left side of the container. + // It can be pixel value: 20, percentage value: 20%, + // or position value: right, center. + Left string + // Distance between title component and the top side of the container. + // It can be pixel value: 20. + Top string + // The font of label + Font *truetype.Font + // The font size of label + FontSize float64 + // The color of label + FontColor Color + // The subtext font size of label + SubtextFontSize float64 + // The subtext font color of label + SubtextFontColor Color +} + +type titleMeasureOption struct { + width int + height int + text string + style Style +} + +func splitTitleText(text string) []string { + arr := strings.Split(text, "\n") + result := make([]string, 0) + for _, v := range arr { + v = strings.TrimSpace(v) + if v == "" { + continue + } + result = append(result, v) + } + return result +} + +type titlePainter struct { + p *Painter + opt *TitleOption +} + +func NewTitlePainter(p *Painter, opt TitleOption) *titlePainter { + return &titlePainter{ + p: p, + opt: &opt, + } +} + +func (t *titlePainter) Render() (Box, error) { + opt := t.opt + p := t.p + theme := opt.Theme + measureOptions := make([]titleMeasureOption, 0) + + if opt.Font == nil { + opt.Font = theme.GetFont() + } + if opt.FontColor.IsZero() { + opt.FontColor = theme.GetTextColor() + } + if opt.FontSize == 0 { + opt.FontSize = theme.GetFontSize() + } + if opt.SubtextFontColor.IsZero() { + opt.SubtextFontColor = opt.FontColor + } + if opt.SubtextFontSize == 0 { + opt.SubtextFontSize = opt.FontSize + } + + titleTextStyle := Style{ + Font: opt.Font, + FontSize: opt.FontSize, + FontColor: opt.FontColor, + } + // 主标题 + for _, v := range splitTitleText(opt.Text) { + measureOptions = append(measureOptions, titleMeasureOption{ + text: v, + style: titleTextStyle, + }) + } + subtextStyle := Style{ + Font: opt.Font, + FontSize: opt.SubtextFontSize, + FontColor: opt.SubtextFontColor, + } + // 副标题 + for _, v := range splitTitleText(opt.Subtext) { + measureOptions = append(measureOptions, titleMeasureOption{ + text: v, + style: subtextStyle, + }) + } + textMaxWidth := 0 + textMaxHeight := 0 + for index, item := range measureOptions { + p.OverrideTextStyle(item.style) + textBox := p.MeasureText(item.text) + + w := textBox.Width() + h := textBox.Height() + if w > textMaxWidth { + textMaxWidth = w + } + if h > textMaxHeight { + textMaxHeight = h + } + measureOptions[index].height = h + measureOptions[index].width = w + } + width := textMaxWidth + + titleX := 0 + switch opt.Left { + case PositionRight: + titleX = p.Width() - textMaxWidth + case PositionCenter: + titleX = p.Width()>>1 - (textMaxWidth >> 1) + default: + if strings.HasSuffix(opt.Left, "%") { + value, _ := strconv.Atoi(strings.ReplaceAll(opt.Left, "%", "")) + titleX = p.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 { + p.OverrideTextStyle(item.style) + x := titleX + (textMaxWidth-item.width)>>1 + y := titleY + item.height + p.Text(item.text, x, y) + titleY += item.height + } + + return Box{ + Bottom: titleY, + Right: titleX + width, + }, nil +} From b69728dd1260336d9816b654e69797b7187dbbc6 Mon Sep 17 00:00:00 2001 From: vicanso Date: Tue, 14 Jun 2022 23:07:11 +0800 Subject: [PATCH 016/116] feat: support bar chart render --- axis.go | 6 +- bar_chart.go | 205 ++++++++++++++++++++++++++++++++++++ charts.go | 30 ++++-- examples/bar_chart/main.go | 127 ++++++++++++++++++++++ examples/line_chart/main.go | 4 +- examples/painter/main.go | 10 +- legend.go | 3 + line_chart.go | 58 ++++++---- mark_point.go | 3 +- xaxis.go | 6 +- yaxis.go | 6 +- 11 files changed, 408 insertions(+), 50 deletions(-) create mode 100644 bar_chart.go create mode 100644 examples/bar_chart/main.go diff --git a/axis.go b/axis.go index bb2e6a3..bd760b6 100644 --- a/axis.go +++ b/axis.go @@ -28,17 +28,17 @@ import ( type axisPainter struct { p *Painter - opt *AxisPainterOption + opt *AxisOption } -func NewAxisPainter(p *Painter, opt AxisPainterOption) *axisPainter { +func NewAxisPainter(p *Painter, opt AxisOption) *axisPainter { return &axisPainter{ p: p, opt: &opt, } } -type AxisPainterOption struct { +type AxisOption struct { // The theme of chart Theme ColorPalette // The label of axis diff --git a/bar_chart.go b/bar_chart.go new file mode 100644 index 0000000..9dadb1e --- /dev/null +++ b/bar_chart.go @@ -0,0 +1,205 @@ +// 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" + "github.com/wcharczuk/go-chart/v2" +) + +type barChart struct { + p *Painter + opt *BarChartOption +} + +func NewBarChart(p *Painter, opt BarChartOption) *barChart { + if opt.Theme == nil { + opt.Theme = NewTheme("") + } + return &barChart{ + p: p, + opt: &opt, + } +} + +type BarChartOption struct { + Theme ColorPalette + // The font size + Font *truetype.Font + // The data series list + SeriesList SeriesList + // The x axis option + XAxis XAxisOption + // The padding of line chart + Padding Box + // The y axis option + YAxisOptions []YAxisOption + // The option of title + Title TitleOption + // The legend option + Legend LegendOption +} + +func (b *barChart) Render() (Box, error) { + p := b.p + opt := b.opt + seriesList := opt.SeriesList + seriesList.init() + renderResult, err := defaultRender(p, defaultRenderOption{ + Theme: opt.Theme, + Padding: opt.Padding, + SeriesList: seriesList, + XAxis: opt.XAxis, + YAxisOptions: opt.YAxisOptions, + TitleOption: opt.Title, + LegendOption: opt.Legend, + }) + if err != nil { + return chart.BoxZero, err + } + seriesPainter := renderResult.seriesPainter + seriesList = seriesList.Filter(ChartTypeBar) + + xRange := NewRange(AxisRangeOption{ + DivideCount: len(opt.XAxis.Data), + Size: seriesPainter.Width(), + }) + x0, x1 := xRange.GetRange(0) + width := int(x1 - x0) + // 每一块之间的margin + margin := 10 + // 每一个bar之间的margin + barMargin := 5 + if width < 20 { + margin = 2 + barMargin = 2 + } else if width < 50 { + margin = 5 + barMargin = 3 + } + seriesCount := len(seriesList) + // 总的宽度-两个margin-(总数-1)的barMargin + barWidth := (width - 2*margin - barMargin*(seriesCount-1)) / len(seriesList) + barMaxHeight := seriesPainter.Height() + theme := opt.Theme + seriesNames := seriesList.Names() + + markPointPainter := NewMarkPointPainter(seriesPainter) + markLinePainter := NewMarkLinePainter(seriesPainter) + rendererList := []Renderer{ + markPointPainter, + markLinePainter, + } + for i := range seriesList { + series := seriesList[i] + yRange := renderResult.axisRanges[series.AxisIndex] + index := series.index + if index == 0 { + index = i + } + seriesColor := theme.GetSeriesColor(index) + + divideValues := xRange.AutoDivide() + points := make([]Point, len(series.Data)) + for j, item := range series.Data { + if j >= xRange.divideCount { + continue + } + x := divideValues[j] + x += margin + if i != 0 { + x += i * (barWidth + barMargin) + } + + h := int(yRange.getHeight(item.Value)) + fillColor := seriesColor + if !item.Style.FillColor.IsZero() { + fillColor = item.Style.FillColor + } + top := barMaxHeight - h + + seriesPainter.OverrideDrawingStyle(Style{ + FillColor: fillColor, + }).Rect(chart.Box{ + Top: top, + Left: x, + Right: x + barWidth, + Bottom: barMaxHeight - 1, + }) + // 用于生成marker point + points[j] = Point{ + // 居中的位置 + X: x + barWidth>>1, + Y: top, + } + // 用于生成marker point + points[j] = Point{ + // 居中的位置 + X: x + barWidth>>1, + Y: top, + } + // 如果label不需要展示,则返回 + if !series.Label.Show { + continue + } + distance := series.Label.Distance + if distance == 0 { + distance = 5 + } + text := NewValueLabelFormater(seriesNames, series.Label.Formatter)(i, item.Value, -1) + labelStyle := Style{ + FontColor: theme.GetTextColor(), + FontSize: labelFontSize, + Font: opt.Font, + } + if !series.Label.Color.IsZero() { + labelStyle.FontColor = series.Label.Color + } + seriesPainter.OverrideTextStyle(labelStyle) + textBox := seriesPainter.MeasureText(text) + seriesPainter.Text(text, x+(barWidth-textBox.Width())>>1, barMaxHeight-h-distance) + } + + markPointPainter.Add(markPointRenderOption{ + FillColor: seriesColor, + Font: opt.Font, + Series: series, + Points: points, + }) + markLinePainter.Add(markLineRenderOption{ + FillColor: seriesColor, + FontColor: opt.Theme.GetTextColor(), + StrokeColor: seriesColor, + Font: opt.Font, + Series: series, + Range: yRange, + }) + } + // 最大、最小的mark point + err = doRender(rendererList...) + if err != nil { + return chart.BoxZero, err + } + + return chart.BoxZero, nil +} diff --git a/charts.go b/charts.go index 947fa8d..09dfc3f 100644 --- a/charts.go +++ b/charts.go @@ -56,14 +56,21 @@ type defaultRenderOption struct { TitleOption TitleOption // The legend option LegendOption LegendOption + // background is filled + backgroundIsFilled bool } type defaultRenderResult struct { axisRanges map[int]axisRange - p *Painter + // 图例区域 + seriesPainter *Painter } func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, error) { + if !opt.backgroundIsFilled { + p.SetBackground(p.Width(), p.Height(), opt.Theme.GetBackgroundColor()) + } + if !opt.Padding.IsZero() { p = p.Child(PainterPaddingOption(opt.Padding)) } @@ -157,8 +164,8 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e return nil, err } - result.p = p.Child(PainterPaddingOption(Box{ - Bottom: rangeHeight, + result.seriesPainter = p.Child(PainterPaddingOption(Box{ + Bottom: defaultXAxisHeight, Left: rangeWidthLeft, Right: rangeWidthRight, })) @@ -200,14 +207,15 @@ func Render(opt ChartOption) (*Painter, error) { lineChartSeriesList := seriesList.Filter(ChartTypeLine) if len(lineChartSeriesList) != 0 { renderer := NewLineChart(p, LineChartOption{ - Theme: opt.theme, - Font: opt.font, - SeriesList: lineChartSeriesList, - XAxis: opt.XAxis, - Padding: opt.Padding, - YAxisOptions: opt.YAxisOptions, - TitleOption: opt.Title, - LegendOption: opt.Legend, + Theme: opt.theme, + Font: opt.font, + SeriesList: lineChartSeriesList, + XAxis: opt.XAxis, + Padding: opt.Padding, + YAxisOptions: opt.YAxisOptions, + Title: opt.Title, + Legend: opt.Legend, + backgroundIsFilled: true, }) rendererList = append(rendererList, renderer) } diff --git a/examples/bar_chart/main.go b/examples/bar_chart/main.go new file mode 100644 index 0000000..5d5da2a --- /dev/null +++ b/examples/bar_chart/main.go @@ -0,0 +1,127 @@ +package main + +import ( + "io/ioutil" + "os" + "path/filepath" + + "github.com/vicanso/go-charts" +) + +func writeFile(buf []byte) error { + tmpPath := "./tmp" + err := os.MkdirAll(tmpPath, 0700) + if err != nil { + return err + } + + file := filepath.Join(tmpPath, "bar-chart.png") + err = ioutil.WriteFile(file, buf, 0600) + if err != nil { + return err + } + return nil +} + +func main() { + p, err := charts.NewPainter(charts.PainterOptions{ + Width: 800, + Height: 600, + Type: charts.ChartOutputPNG, + }) + if err != nil { + panic(err) + } + _, err = charts.NewBarChart(p, charts.BarChartOption{ + Title: charts.TitleOption{ + Text: "Rainfall vs Evaporation", + Subtext: "Fake Data", + }, + Padding: charts.Box{ + Top: 20, + Right: 20, + Bottom: 20, + Left: 20, + }, + XAxis: charts.NewXAxisOption([]string{ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + }), + Legend: charts.NewLegendOption([]string{ + "Rainfall", + "Evaporation", + }, charts.PositionRight), + SeriesList: []charts.Series{ + { + Type: charts.ChartTypeBar, + Data: charts.NewSeriesDataFromValues([]float64{ + 2.0, + 4.9, + 7.0, + 23.2, + 25.6, + 76.7, + 135.6, + 162.2, + 32.6, + 20.0, + 6.4, + 3.3, + }), + MarkPoint: charts.NewMarkPoint( + charts.SeriesMarkDataTypeMax, + charts.SeriesMarkDataTypeMin, + ), + MarkLine: charts.NewMarkLine( + charts.SeriesMarkDataTypeAverage, + ), + }, + { + Type: charts.ChartTypeBar, + Data: charts.NewSeriesDataFromValues([]float64{ + 2.6, + 5.9, + 9.0, + 26.4, + 28.7, + 70.7, + 175.6, + 182.2, + 48.7, + 18.8, + 6.0, + 2.3, + }), + MarkPoint: charts.NewMarkPoint( + charts.SeriesMarkDataTypeMax, + charts.SeriesMarkDataTypeMin, + ), + MarkLine: charts.NewMarkLine( + charts.SeriesMarkDataTypeAverage, + ), + }, + }, + }).Render() + if err != nil { + panic(err) + } + + buf, err := p.Bytes() + if err != nil { + panic(err) + } + err = writeFile(buf) + if err != nil { + panic(err) + } +} diff --git a/examples/line_chart/main.go b/examples/line_chart/main.go index c168f08..414f676 100644 --- a/examples/line_chart/main.go +++ b/examples/line_chart/main.go @@ -39,10 +39,10 @@ func main() { Right: 10, Bottom: 10, }, - TitleOption: charts.TitleOption{ + Title: charts.TitleOption{ Text: "Line", }, - LegendOption: charts.LegendOption{ + Legend: charts.LegendOption{ Data: []string{ "Email", "Union Ads", diff --git a/examples/painter/main.go b/examples/painter/main.go index 5022584..cf2bb81 100644 --- a/examples/painter/main.go +++ b/examples/painter/main.go @@ -490,7 +490,7 @@ func main() { Left: 1, Right: p.Width() - 1, Bottom: top + 50, - })), charts.AxisPainterOption{ + })), charts.AxisOption{ Data: []string{ "Mon", "Tue", @@ -512,7 +512,7 @@ func main() { Left: 1, Right: p.Width() - 1, Bottom: top + 50, - })), charts.AxisPainterOption{ + })), charts.AxisOption{ Position: charts.PositionTop, BoundaryGap: charts.FalseFlag(), Data: []string{ @@ -536,7 +536,7 @@ func main() { Left: 10, Right: 60, Bottom: top + 200, - })), charts.AxisPainterOption{ + })), charts.AxisOption{ Position: charts.PositionLeft, Data: []string{ "Mon", @@ -557,7 +557,7 @@ func main() { Left: 100, Right: 150, Bottom: top + 200, - })), charts.AxisPainterOption{ + })), charts.AxisOption{ Position: charts.PositionRight, Data: []string{ "Mon", @@ -579,7 +579,7 @@ func main() { Left: 150, Right: 300, Bottom: top + 200, - })), charts.AxisPainterOption{ + })), charts.AxisOption{ BoundaryGap: charts.FalseFlag(), Position: charts.PositionLeft, Data: []string{ diff --git a/legend.go b/legend.go index b8a6fdc..e645e17 100644 --- a/legend.go +++ b/legend.go @@ -87,6 +87,9 @@ func (l *legendPainter) Render() (Box, error) { if opt.FontColor.IsZero() { opt.FontColor = theme.GetTextColor() } + if opt.Left == "" { + opt.Left = PositionCenter + } p := l.p p.SetTextStyle(Style{ FontSize: opt.FontSize, diff --git a/line_chart.go b/line_chart.go index 5f4ea4f..451edfe 100644 --- a/line_chart.go +++ b/line_chart.go @@ -56,9 +56,11 @@ type LineChartOption struct { // The y axis option YAxisOptions []YAxisOption // The option of title - TitleOption TitleOption + Title TitleOption // The legend option - LegendOption LegendOption + Legend LegendOption + // background is filled + backgroundIsFilled bool } func (l *lineChart) Render() (Box, error) { @@ -67,26 +69,39 @@ func (l *lineChart) Render() (Box, error) { seriesList := opt.SeriesList seriesList.init() renderResult, err := defaultRender(p, defaultRenderOption{ - Theme: opt.Theme, - Padding: opt.Padding, - SeriesList: seriesList, - XAxis: opt.XAxis, - YAxisOptions: opt.YAxisOptions, - TitleOption: opt.TitleOption, - LegendOption: opt.LegendOption, + Theme: opt.Theme, + Padding: opt.Padding, + SeriesList: seriesList, + XAxis: opt.XAxis, + YAxisOptions: opt.YAxisOptions, + TitleOption: opt.Title, + LegendOption: opt.Legend, + backgroundIsFilled: opt.backgroundIsFilled, }) if err != nil { return chart.BoxZero, err } + boundaryGap := true + if opt.XAxis.BoundaryGap != nil && !*opt.XAxis.BoundaryGap { + boundaryGap = false + } seriesList = seriesList.Filter(ChartTypeLine) - seriesPainter := renderResult.p + seriesPainter := renderResult.seriesPainter - xDivideValues := autoDivide(seriesPainter.Width(), len(opt.XAxis.Data)) + xDivideCount := len(opt.XAxis.Data) + if !boundaryGap { + xDivideCount-- + } + xDivideValues := autoDivide(seriesPainter.Width(), xDivideCount) xValues := make([]int, len(xDivideValues)-1) - for i := 0; i < len(xDivideValues)-1; i++ { - xValues[i] = (xDivideValues[i] + xDivideValues[i+1]) >> 1 + if boundaryGap { + for i := 0; i < len(xDivideValues)-1; i++ { + xValues[i] = (xDivideValues[i] + xDivideValues[i+1]) >> 1 + } + } else { + xValues = xDivideValues } markPointPainter := NewMarkPointPainter(seriesPainter) markLinePainter := NewMarkLinePainter(seriesPainter) @@ -94,7 +109,8 @@ func (l *lineChart) Render() (Box, error) { markPointPainter, markLinePainter, } - for index, series := range seriesList { + for index := range seriesList { + series := seriesList[index] seriesColor := opt.Theme.GetSeriesColor(index) drawingStyle := Style{ StrokeColor: seriesColor, @@ -102,10 +118,10 @@ func (l *lineChart) Render() (Box, error) { } seriesPainter.SetDrawingStyle(drawingStyle) - yr := renderResult.axisRanges[series.AxisIndex] + yRange := renderResult.axisRanges[series.AxisIndex] points := make([]Point, 0) for i, item := range series.Data { - h := yr.getRestHeight(item.Value) + h := yRange.getRestHeight(item.Value) p := Point{ X: xValues[i], Y: h, @@ -136,15 +152,13 @@ func (l *lineChart) Render() (Box, error) { StrokeColor: seriesColor, Font: opt.Font, Series: series, - Range: yr, + Range: yRange, }) } // 最大、最小的mark point - for _, renderer := range rendererList { - _, err = renderer.Render() - if err != nil { - return chart.BoxZero, err - } + err = doRender(rendererList...) + if err != nil { + return chart.BoxZero, err } return p.box, nil diff --git a/mark_point.go b/mark_point.go index ce3cb0f..07daf57 100644 --- a/mark_point.go +++ b/mark_point.go @@ -64,6 +64,7 @@ func NewMarkPointPainter(p *Painter) *markPointPainter { func (m *markPointPainter) Render() (Box, error) { painter := m.p + theme := m.p.theme for _, opt := range m.options { s := opt.Series if len(s.MarkPoint.Data) == 0 { @@ -78,7 +79,7 @@ func (m *markPointPainter) Render() (Box, error) { painter.OverrideDrawingStyle(Style{ FillColor: opt.FillColor, }).OverrideTextStyle(Style{ - FontColor: NewTheme(ThemeDark).GetTextColor(), + FontColor: theme.GetTextColor(), FontSize: labelFontSize, StrokeWidth: 1, Font: opt.Font, diff --git a/xaxis.go b/xaxis.go index a8c28c0..d8f6700 100644 --- a/xaxis.go +++ b/xaxis.go @@ -62,12 +62,12 @@ func NewXAxisOption(data []string, boundaryGap ...*bool) XAxisOption { return opt } -func (opt *XAxisOption) ToAxisPainterOption() AxisPainterOption { +func (opt *XAxisOption) ToAxisOption() AxisOption { position := PositionBottom if opt.Position == PositionTop { position = PositionTop } - return AxisPainterOption{ + return AxisOption{ Theme: opt.Theme, Data: opt.Data, BoundaryGap: opt.BoundaryGap, @@ -84,5 +84,5 @@ func NewBottomXAxis(p *Painter, opt XAxisOption) *axisPainter { p = p.Child(PainterPaddingOption(Box{ Top: p.Height() - defaultXAxisHeight, })) - return NewAxisPainter(p, opt.ToAxisPainterOption()) + return NewAxisPainter(p, opt.ToAxisOption()) } diff --git a/yaxis.go b/yaxis.go index 653f6ec..b011a74 100644 --- a/yaxis.go +++ b/yaxis.go @@ -39,12 +39,12 @@ type YAxisOption struct { FontColor Color } -func (opt *YAxisOption) ToAxisPainterOption() AxisPainterOption { +func (opt *YAxisOption) ToAxisOption() AxisOption { position := PositionLeft if opt.Position == PositionRight { position = PositionRight } - return AxisPainterOption{ + return AxisOption{ Theme: opt.Theme, Data: opt.Data, Position: position, @@ -62,5 +62,5 @@ func NewLeftYAxis(p *Painter, opt YAxisOption) *axisPainter { p = p.Child(PainterPaddingOption(Box{ Bottom: defaultXAxisHeight, })) - return NewAxisPainter(p, opt.ToAxisPainterOption()) + return NewAxisPainter(p, opt.ToAxisOption()) } From 3f245215931296b1de0f5c30fd7363a147a3c79a Mon Sep 17 00:00:00 2001 From: vicanso Date: Wed, 15 Jun 2022 23:30:37 +0800 Subject: [PATCH 017/116] feat: support horizontal bar chart --- alias.go | 4 + axis.go | 25 +++- bar_chart.go | 46 +++--- charts.go | 119 +++++++++++++--- examples/charts/main.go | 195 ++++++++++++++++++++++++++ examples/horizontal_bar_chart/main.go | 94 +++++++++++++ horizontal_bar_chart.go | 152 ++++++++++++++++++++ legend.go | 8 +- line_chart.go | 50 ++++--- mark_line.go | 3 +- mark_point.go | 3 +- series.go | 6 + title.go | 5 + xaxis.go | 33 +++-- yaxis.go | 25 +++- 15 files changed, 677 insertions(+), 91 deletions(-) create mode 100644 examples/horizontal_bar_chart/main.go create mode 100644 horizontal_bar_chart.go diff --git a/alias.go b/alias.go index 0b161e6..a96f50b 100644 --- a/alias.go +++ b/alias.go @@ -31,6 +31,8 @@ type Box = chart.Box type Style = chart.Style type Color = drawing.Color +var BoxZero = chart.BoxZero + type Point struct { X int Y int @@ -42,6 +44,8 @@ const ( ChartTypePie = "pie" ChartTypeRadar = "radar" ChartTypeFunnel = "funnel" + // horizontal bar + ChartTypeHorizontalBar = "horizontalBar" ) const ( diff --git a/axis.go b/axis.go index bd760b6..d069c39 100644 --- a/axis.go +++ b/axis.go @@ -153,6 +153,8 @@ func (a *axisPainter) Render() (Box, error) { padding.Right = top.Width() - width case PositionRight: padding.Left = top.Width() - width + default: + padding.Top = top.Height() - defaultXAxisHeight } p := top.Child(PainterPaddingOption(padding)) @@ -240,7 +242,10 @@ func (a *axisPainter) Render() (Box, error) { x0 = 0 x1 = top.Width() - p.Width() } - for _, y := range autoDivide(height, tickCount) { + for index, y := range autoDivide(height, tickCount) { + if index == 0 { + continue + } top.LineStroke([]Point{ { X: x0, @@ -252,6 +257,24 @@ func (a *axisPainter) Render() (Box, error) { }, }) } + } else { + y0 := p.Height() - defaultXAxisHeight + y1 := top.Height() - defaultXAxisHeight + for index, x := range autoDivide(width, tickCount) { + if index == 0 { + continue + } + top.LineStroke([]Point{ + { + X: x, + Y: y0, + }, + { + X: x, + Y: y1, + }, + }) + } } } diff --git a/bar_chart.go b/bar_chart.go index 9dadb1e..8fae4df 100644 --- a/bar_chart.go +++ b/bar_chart.go @@ -60,25 +60,10 @@ type BarChartOption struct { Legend LegendOption } -func (b *barChart) Render() (Box, error) { +func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) { p := b.p opt := b.opt - seriesList := opt.SeriesList - seriesList.init() - renderResult, err := defaultRender(p, defaultRenderOption{ - Theme: opt.Theme, - Padding: opt.Padding, - SeriesList: seriesList, - XAxis: opt.XAxis, - YAxisOptions: opt.YAxisOptions, - TitleOption: opt.Title, - LegendOption: opt.Legend, - }) - if err != nil { - return chart.BoxZero, err - } - seriesPainter := renderResult.seriesPainter - seriesList = seriesList.Filter(ChartTypeBar) + seriesPainter := result.seriesPainter xRange := NewRange(AxisRangeOption{ DivideCount: len(opt.XAxis.Data), @@ -112,7 +97,7 @@ func (b *barChart) Render() (Box, error) { } for i := range seriesList { series := seriesList[i] - yRange := renderResult.axisRanges[series.AxisIndex] + yRange := result.axisRanges[series.AxisIndex] index := series.index if index == 0 { index = i @@ -196,10 +181,29 @@ func (b *barChart) Render() (Box, error) { }) } // 最大、最小的mark point - err = doRender(rendererList...) + err := doRender(rendererList...) if err != nil { - return chart.BoxZero, err + return BoxZero, err } - return chart.BoxZero, nil + return p.box, nil +} + +func (b *barChart) Render() (Box, error) { + p := b.p + opt := b.opt + renderResult, err := defaultRender(p, defaultRenderOption{ + Theme: opt.Theme, + Padding: opt.Padding, + SeriesList: opt.SeriesList, + XAxis: opt.XAxis, + YAxisOptions: opt.YAxisOptions, + TitleOption: opt.Title, + LegendOption: opt.Legend, + }) + if err != nil { + return BoxZero, err + } + seriesList := opt.SeriesList.Filter(ChartTypeLine) + return b.render(renderResult, seriesList) } diff --git a/charts.go b/charts.go index 09dfc3f..ae14c8d 100644 --- a/charts.go +++ b/charts.go @@ -22,6 +22,8 @@ package charts +import "errors" + const labelFontSize = 10 const defaultDotWidth = 2.0 const defaultStrokeWidth = 2.0 @@ -44,6 +46,28 @@ type Renderer interface { Render() (Box, error) } +type renderHandler struct { + list []func() error +} + +func (rh *renderHandler) Add(fn func() error) { + list := rh.list + if len(list) == 0 { + list = make([]func() error, 0) + } + rh.list = append(list, fn) +} + +func (rh *renderHandler) Do() error { + for _, fn := range rh.list { + err := fn() + if err != nil { + return err + } + } + return nil +} + type defaultRenderOption struct { Theme ColorPalette Padding Box @@ -58,6 +82,8 @@ type defaultRenderOption struct { LegendOption LegendOption // background is filled backgroundIsFilled bool + // x y axis is reversed + axisReversed bool } type defaultRenderResult struct { @@ -67,6 +93,8 @@ type defaultRenderResult struct { } func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, error) { + seriesList := opt.SeriesList + seriesList.init() if !opt.backgroundIsFilled { p.SetBackground(p.Width(), p.Height(), opt.Theme.GetBackgroundColor()) } @@ -138,7 +166,13 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e if yAxisOption.Theme == nil { yAxisOption.Theme = opt.Theme } - yAxisOption.Data = r.Values() + if !opt.axisReversed { + yAxisOption.Data = r.Values() + } else { + yAxisOption.isCategoryAxis = true + opt.XAxis.Data = r.Values() + opt.XAxis.isValueAxis = true + } reverseStringSlice(yAxisOption.Data) // TODO生成其它位置既yAxis yAxis := NewLeftYAxis(p, yAxisOption) @@ -201,30 +235,71 @@ func Render(opt ChartOption) (*Painter, error) { seriesList := opt.SeriesList seriesList.init() - rendererList := make([]Renderer, 0) - // line chart - lineChartSeriesList := seriesList.Filter(ChartTypeLine) - if len(lineChartSeriesList) != 0 { - renderer := NewLineChart(p, LineChartOption{ - Theme: opt.theme, - Font: opt.font, - SeriesList: lineChartSeriesList, - XAxis: opt.XAxis, - Padding: opt.Padding, - YAxisOptions: opt.YAxisOptions, - Title: opt.Title, - Legend: opt.Legend, - backgroundIsFilled: true, - }) - rendererList = append(rendererList, renderer) + lineSeriesList := seriesList.Filter(ChartTypeLine) + barSeriesList := seriesList.Filter(ChartTypeBar) + horizontalBarSeriesList := seriesList.Filter(ChartTypeHorizontalBar) + if len(horizontalBarSeriesList) != 0 && len(horizontalBarSeriesList) != len(seriesList) { + return nil, errors.New("Horizontal bar can not mix other charts") } - for _, renderer := range rendererList { - _, err := renderer.Render() - if err != nil { - return nil, err - } + axisReversed := len(horizontalBarSeriesList) != 0 + + renderResult, err := defaultRender(p, defaultRenderOption{ + Theme: opt.theme, + Padding: opt.Padding, + SeriesList: opt.SeriesList, + XAxis: opt.XAxis, + YAxisOptions: opt.YAxisOptions, + TitleOption: opt.Title, + LegendOption: opt.Legend, + axisReversed: axisReversed, + }) + if err != nil { + return nil, err + } + + handler := renderHandler{} + + if len(lineSeriesList) != 0 { + handler.Add(func() error { + _, err := NewLineChart(p, LineChartOption{ + Theme: opt.theme, + Font: opt.font, + XAxis: opt.XAxis, + }).render(renderResult, lineSeriesList) + return err + }) + } + + // bar chart + if len(barSeriesList) != 0 { + handler.Add(func() error { + _, err := NewBarChart(p, BarChartOption{ + Theme: opt.theme, + Font: opt.font, + XAxis: opt.XAxis, + }).render(renderResult, barSeriesList) + return err + }) + } + + // horizontal bar chart + if len(horizontalBarSeriesList) != 0 { + handler.Add(func() error { + _, err := NewHorizontalBarChart(p, HorizontalBarChartOption{ + Theme: opt.theme, + Font: opt.font, + YAxisOptions: opt.YAxisOptions, + }).render(renderResult, horizontalBarSeriesList) + return err + }) + } + + err = handler.Do() + + if err != nil { + return nil, err } return p, nil diff --git a/examples/charts/main.go b/examples/charts/main.go index 18f5a95..c7986a4 100644 --- a/examples/charts/main.go +++ b/examples/charts/main.go @@ -218,6 +218,201 @@ func indexHandler(w http.ResponseWriter, req *http.Request) { }, }, }, + // 柱状图 + { + Title: charts.TitleOption{ + Text: "Bar", + }, + XAxis: charts.NewXAxisOption([]string{ + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun", + }), + Legend: charts.LegendOption{ + Data: []string{ + "Rainfall", + "Evaporation", + }, + Icon: charts.IconRect, + }, + SeriesList: []charts.Series{ + charts.NewSeriesFromValues([]float64{ + 120, + 200, + 150, + 80, + 70, + 110, + 130, + }, charts.ChartTypeBar), + { + Type: charts.ChartTypeBar, + Data: []charts.SeriesData{ + { + Value: 100, + }, + { + Value: 190, + Style: charts.Style{ + FillColor: charts.Color{ + R: 169, + G: 0, + B: 0, + A: 255, + }, + }, + }, + { + Value: 230, + }, + { + Value: 140, + }, + { + Value: 100, + }, + { + Value: 200, + }, + { + Value: 180, + }, + }, + }, + }, + }, + // 水平柱状图 + { + Title: charts.TitleOption{ + Text: "World Population", + }, + Padding: charts.Box{ + Top: 20, + Right: 40, + Bottom: 20, + Left: 20, + }, + Legend: charts.NewLegendOption([]string{ + "2011", + "2012", + }), + YAxisOptions: charts.NewYAxisOptions([]string{ + "Brazil", + "Indonesia", + "USA", + "India", + "China", + "World", + }), + SeriesList: []charts.Series{ + { + Type: charts.ChartTypeHorizontalBar, + Data: charts.NewSeriesDataFromValues([]float64{ + 18203, + 23489, + 29034, + 104970, + 131744, + 630230, + }), + }, + { + Type: charts.ChartTypeHorizontalBar, + Data: charts.NewSeriesDataFromValues([]float64{ + 19325, + 23438, + 31000, + 121594, + 134141, + 681807, + }), + }, + }, + }, + { + Title: charts.TitleOption{ + Text: "Rainfall vs Evaporation", + Subtext: "Fake Data", + }, + Padding: charts.Box{ + Top: 20, + Right: 20, + Bottom: 20, + Left: 20, + }, + XAxis: charts.NewXAxisOption([]string{ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + }), + Legend: charts.NewLegendOption([]string{ + "Rainfall", + "Evaporation", + }, charts.PositionRight), + SeriesList: []charts.Series{ + { + Type: charts.ChartTypeBar, + Data: charts.NewSeriesDataFromValues([]float64{ + 2.0, + 4.9, + 7.0, + 23.2, + 25.6, + 76.7, + 135.6, + 162.2, + 32.6, + 20.0, + 6.4, + 3.3, + }), + MarkPoint: charts.NewMarkPoint( + charts.SeriesMarkDataTypeMax, + charts.SeriesMarkDataTypeMin, + ), + MarkLine: charts.NewMarkLine( + charts.SeriesMarkDataTypeAverage, + ), + }, + { + Type: charts.ChartTypeBar, + Data: charts.NewSeriesDataFromValues([]float64{ + 2.6, + 5.9, + 9.0, + 26.4, + 28.7, + 70.7, + 175.6, + 182.2, + 48.7, + 18.8, + 6.0, + 2.3, + }), + MarkPoint: charts.NewMarkPoint( + charts.SeriesMarkDataTypeMax, + charts.SeriesMarkDataTypeMin, + ), + MarkLine: charts.NewMarkLine( + charts.SeriesMarkDataTypeAverage, + ), + }, + }, + }, } handler(w, req, chartOptions, nil) } diff --git a/examples/horizontal_bar_chart/main.go b/examples/horizontal_bar_chart/main.go new file mode 100644 index 0000000..eecd9ec --- /dev/null +++ b/examples/horizontal_bar_chart/main.go @@ -0,0 +1,94 @@ +package main + +import ( + "io/ioutil" + "os" + "path/filepath" + + "github.com/vicanso/go-charts" +) + +func writeFile(buf []byte) error { + tmpPath := "./tmp" + err := os.MkdirAll(tmpPath, 0700) + if err != nil { + return err + } + + file := filepath.Join(tmpPath, "horizontal-bar-chart.png") + err = ioutil.WriteFile(file, buf, 0600) + if err != nil { + return err + } + return nil +} + +func main() { + p, err := charts.NewPainter(charts.PainterOptions{ + Width: 800, + Height: 600, + Type: charts.ChartOutputPNG, + }) + if err != nil { + panic(err) + } + _, err = charts.NewHorizontalBarChart(p, charts.HorizontalBarChartOption{ + Title: charts.TitleOption{ + Text: "World Population", + }, + Padding: charts.Box{ + Top: 20, + Right: 40, + Bottom: 20, + Left: 20, + }, + Legend: charts.NewLegendOption([]string{ + "2011", + "2012", + }), + YAxisOptions: charts.NewYAxisOptions([]string{ + "Brazil", + "Indonesia", + "USA", + "India", + "China", + "World", + }), + SeriesList: []charts.Series{ + { + Type: charts.ChartTypeHorizontalBar, + Data: charts.NewSeriesDataFromValues([]float64{ + 18203, + 23489, + 29034, + 104970, + 131744, + 630230, + }), + }, + { + Type: charts.ChartTypeHorizontalBar, + Data: charts.NewSeriesDataFromValues([]float64{ + 19325, + 23438, + 31000, + 121594, + 134141, + 681807, + }), + }, + }, + }).Render() + if err != nil { + panic(err) + } + + buf, err := p.Bytes() + if err != nil { + panic(err) + } + err = writeFile(buf) + if err != nil { + panic(err) + } +} diff --git a/horizontal_bar_chart.go b/horizontal_bar_chart.go new file mode 100644 index 0000000..87ca9ae --- /dev/null +++ b/horizontal_bar_chart.go @@ -0,0 +1,152 @@ +// 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" + "github.com/wcharczuk/go-chart/v2" +) + +type horizontalBarChart struct { + p *Painter + opt *HorizontalBarChartOption +} + +type HorizontalBarChartOption struct { + Theme ColorPalette + // The font size + Font *truetype.Font + // The data series list + SeriesList SeriesList + // The x axis option + XAxis XAxisOption + // The padding of line chart + Padding Box + // The y axis option + YAxisOptions []YAxisOption + // The option of title + Title TitleOption + // The legend option + Legend LegendOption +} + +func NewHorizontalBarChart(p *Painter, opt HorizontalBarChartOption) *horizontalBarChart { + if opt.Theme == nil { + opt.Theme = NewTheme("") + } + return &horizontalBarChart{ + p: p, + opt: &opt, + } +} + +func (h *horizontalBarChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) { + p := h.p + opt := h.opt + seriesPainter := result.seriesPainter + yRange := result.axisRanges[0] + y0, y1 := yRange.GetRange(0) + height := int(y1 - y0) + // 每一块之间的margin + margin := 10 + // 每一个bar之间的margin + barMargin := 5 + if height < 20 { + margin = 2 + barMargin = 2 + } else if height < 50 { + margin = 5 + barMargin = 3 + } + seriesCount := len(seriesList) + // 总的高度-两个margin-(总数-1)的barMargin + barHeight := (height - 2*margin - barMargin*(seriesCount-1)) / len(seriesList) + + theme := opt.Theme + + max, min := seriesList.GetMaxMin(0) + xRange := NewRange(AxisRangeOption{ + Min: min, + Max: max, + DivideCount: defaultAxisDivideCount, + Size: seriesPainter.Width(), + }) + + for i := range seriesList { + series := seriesList[i] + index := series.index + if index == 0 { + index = i + } + seriesColor := theme.GetSeriesColor(index) + divideValues := yRange.AutoDivide() + for j, item := range series.Data { + if j >= yRange.divideCount { + continue + } + // 显示位置切换 + j = yRange.divideCount - j - 1 + y := divideValues[j] + y += margin + if i != 0 { + y += i * (barHeight + barMargin) + } + + w := int(xRange.getHeight(item.Value)) + fillColor := seriesColor + if !item.Style.FillColor.IsZero() { + fillColor = item.Style.FillColor + } + right := w + seriesPainter.OverrideDrawingStyle(Style{ + FillColor: fillColor, + }).Rect(chart.Box{ + Top: y, + Left: 0, + Right: right, + Bottom: y + barHeight, + }) + } + } + return p.box, nil +} + +func (h *horizontalBarChart) Render() (Box, error) { + p := h.p + opt := h.opt + renderResult, err := defaultRender(p, defaultRenderOption{ + Theme: opt.Theme, + Padding: opt.Padding, + SeriesList: opt.SeriesList, + XAxis: opt.XAxis, + YAxisOptions: opt.YAxisOptions, + TitleOption: opt.Title, + LegendOption: opt.Legend, + axisReversed: true, + }) + if err != nil { + return BoxZero, err + } + seriesList := opt.SeriesList.Filter(ChartTypeHorizontalBar) + return h.render(renderResult, seriesList) +} diff --git a/legend.go b/legend.go index e645e17..65793c9 100644 --- a/legend.go +++ b/legend.go @@ -155,17 +155,17 @@ func (l *legendPainter) Render() (Box, error) { drawIcon := func(top, left int) int { if opt.Icon == IconRect { p.Rect(Box{ - Top: top - legendHeight + 4, + Top: top - legendHeight + 8, Left: left, Right: left + legendWidth, - Bottom: top - 2, + Bottom: top + 1, }) } else { p.LegendLineDot(Box{ - Top: top, + Top: top + 1, Left: left, Right: left + legendWidth, - Bottom: top + legendHeight, + Bottom: top + legendHeight + 1, }) } return left + legendWidth diff --git a/line_chart.go b/line_chart.go index 451edfe..47a497f 100644 --- a/line_chart.go +++ b/line_chart.go @@ -24,7 +24,6 @@ package charts import ( "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2" "github.com/wcharczuk/go-chart/v2/drawing" ) @@ -63,32 +62,15 @@ type LineChartOption struct { backgroundIsFilled bool } -func (l *lineChart) Render() (Box, error) { +func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) { p := l.p opt := l.opt - seriesList := opt.SeriesList - seriesList.init() - renderResult, err := defaultRender(p, defaultRenderOption{ - Theme: opt.Theme, - Padding: opt.Padding, - SeriesList: seriesList, - XAxis: opt.XAxis, - YAxisOptions: opt.YAxisOptions, - TitleOption: opt.Title, - LegendOption: opt.Legend, - backgroundIsFilled: opt.backgroundIsFilled, - }) - if err != nil { - return chart.BoxZero, err - } boundaryGap := true if opt.XAxis.BoundaryGap != nil && !*opt.XAxis.BoundaryGap { boundaryGap = false } - seriesList = seriesList.Filter(ChartTypeLine) - - seriesPainter := renderResult.seriesPainter + seriesPainter := result.seriesPainter xDivideCount := len(opt.XAxis.Data) if !boundaryGap { @@ -118,7 +100,7 @@ func (l *lineChart) Render() (Box, error) { } seriesPainter.SetDrawingStyle(drawingStyle) - yRange := renderResult.axisRanges[series.AxisIndex] + yRange := result.axisRanges[series.AxisIndex] points := make([]Point, 0) for i, item := range series.Data { h := yRange.getRestHeight(item.Value) @@ -156,10 +138,32 @@ func (l *lineChart) Render() (Box, error) { }) } // 最大、最小的mark point - err = doRender(rendererList...) + err := doRender(rendererList...) if err != nil { - return chart.BoxZero, err + return BoxZero, err } return p.box, nil } + +func (l *lineChart) Render() (Box, error) { + p := l.p + opt := l.opt + + renderResult, err := defaultRender(p, defaultRenderOption{ + Theme: opt.Theme, + Padding: opt.Padding, + SeriesList: opt.SeriesList, + XAxis: opt.XAxis, + YAxisOptions: opt.YAxisOptions, + TitleOption: opt.Title, + LegendOption: opt.Legend, + backgroundIsFilled: opt.backgroundIsFilled, + }) + if err != nil { + return BoxZero, err + } + seriesList := opt.SeriesList.Filter(ChartTypeLine) + + return l.render(renderResult, seriesList) +} diff --git a/mark_line.go b/mark_line.go index 9a9d568..bb1b602 100644 --- a/mark_line.go +++ b/mark_line.go @@ -24,7 +24,6 @@ package charts import ( "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2" ) func NewMarkLine(markLineTypes ...string) SeriesMarkLine { @@ -104,7 +103,7 @@ func (m *markLinePainter) Render() (Box, error) { painter.Text(text, width, y+textBox.Height()>>1-2) } } - return chart.BoxZero, nil + return BoxZero, nil } func markLineRender(opt markLineRenderOption) { diff --git a/mark_point.go b/mark_point.go index 07daf57..3d43a73 100644 --- a/mark_point.go +++ b/mark_point.go @@ -24,7 +24,6 @@ package charts import ( "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2" ) func NewMarkPoint(markPointTypes ...string) SeriesMarkPoint { @@ -99,5 +98,5 @@ func (m *markPointPainter) Render() (Box, error) { painter.Text(text, p.X-textBox.Width()>>1, p.Y-symbolSize>>1-2) } } - return chart.BoxZero, nil + return BoxZero, nil } diff --git a/series.go b/series.go index 2888f30..4808bcb 100644 --- a/series.go +++ b/series.go @@ -132,6 +132,12 @@ func (sl SeriesList) init() { } } +func (sl SeriesList) reverse() { + for i, j := 0, len(sl)-1; i < j; i, j = i+1, j-1 { + sl[i], sl[j] = sl[j], sl[i] + } +} + func (sl SeriesList) Filter(chartType string) SeriesList { arr := make(SeriesList, 0) for index, item := range sl { diff --git a/title.go b/title.go index 30831ac..a805c55 100644 --- a/title.go +++ b/title.go @@ -95,6 +95,11 @@ func (t *titlePainter) Render() (Box, error) { opt := t.opt p := t.p theme := opt.Theme + + if opt.Text == "" && opt.Subtext == "" { + return BoxZero, nil + } + measureOptions := make([]titleMeasureOption, 0) if opt.Font == nil { diff --git a/xaxis.go b/xaxis.go index d8f6700..f06d71f 100644 --- a/xaxis.go +++ b/xaxis.go @@ -47,7 +47,8 @@ type XAxisOption struct { // The line color of axis StrokeColor Color // The color of label - FontColor Color + FontColor Color + isValueAxis bool } const defaultXAxisHeight = 30 @@ -67,22 +68,26 @@ func (opt *XAxisOption) ToAxisOption() AxisOption { if opt.Position == PositionTop { position = PositionTop } - return AxisOption{ - Theme: opt.Theme, - Data: opt.Data, - BoundaryGap: opt.BoundaryGap, - Position: position, - SplitNumber: opt.SplitNumber, - StrokeColor: opt.StrokeColor, - FontSize: opt.FontSize, - Font: opt.Font, - FontColor: opt.FontColor, + axisOpt := AxisOption{ + Theme: opt.Theme, + Data: opt.Data, + BoundaryGap: opt.BoundaryGap, + Position: position, + SplitNumber: opt.SplitNumber, + StrokeColor: opt.StrokeColor, + FontSize: opt.FontSize, + Font: opt.Font, + FontColor: opt.FontColor, + SplitLineColor: opt.Theme.GetAxisSplitLineColor(), } + if opt.isValueAxis { + axisOpt.SplitLineShow = true + axisOpt.StrokeWidth = -1 + axisOpt.BoundaryGap = FalseFlag() + } + return axisOpt } func NewBottomXAxis(p *Painter, opt XAxisOption) *axisPainter { - p = p.Child(PainterPaddingOption(Box{ - Top: p.Height() - defaultXAxisHeight, - })) return NewAxisPainter(p, opt.ToAxisOption()) } diff --git a/yaxis.go b/yaxis.go index b011a74..609924f 100644 --- a/yaxis.go +++ b/yaxis.go @@ -36,7 +36,22 @@ type YAxisOption struct { // The position of axis, it can be 'left' or 'right' Position string // The color of label - FontColor Color + FontColor Color + isCategoryAxis bool +} + +func NewYAxisOptions(data []string, others ...[]string) []YAxisOption { + arr := [][]string{ + data, + } + arr = append(arr, others...) + opts := make([]YAxisOption, 0) + for _, data := range arr { + opts = append(opts, YAxisOption{ + Data: data, + }) + } + return opts } func (opt *YAxisOption) ToAxisOption() AxisOption { @@ -44,7 +59,7 @@ func (opt *YAxisOption) ToAxisOption() AxisOption { if opt.Position == PositionRight { position = PositionRight } - return AxisOption{ + axisOpt := AxisOption{ Theme: opt.Theme, Data: opt.Data, Position: position, @@ -56,6 +71,12 @@ func (opt *YAxisOption) ToAxisOption() AxisOption { SplitLineShow: true, SplitLineColor: opt.Theme.GetAxisSplitLineColor(), } + if opt.isCategoryAxis { + axisOpt.BoundaryGap = TrueFlag() + axisOpt.StrokeWidth = 1 + axisOpt.SplitLineShow = false + } + return axisOpt } func NewLeftYAxis(p *Painter, opt YAxisOption) *axisPainter { From 65a1cb11adfda2466a77ecb333fd3f216467993d Mon Sep 17 00:00:00 2001 From: vicanso Date: Thu, 16 Jun 2022 23:08:20 +0800 Subject: [PATCH 018/116] feat: support pie, radar and funnel chart --- axis.go | 34 ++- bar_chart.go | 16 +- chart_option.go | 2 +- charts.go | 150 ++++++++-- echarts.go | 505 ++++++++++++++++++++++++++++++++++ examples/charts/main.go | 372 +++++++++++++++++++++++-- examples/funnel_chart/main.go | 97 +++++++ examples/pie_chart/main.go | 83 ++++++ examples/radar_chart/main.go | 112 ++++++++ funnel_chart.go | 172 ++++++++++++ horizontal_bar_chart.go | 14 +- legend.go | 21 +- line_chart.go | 2 +- painter.go | 3 + pie_chart.go | 211 ++++++++++++++ radar_chart.go | 245 +++++++++++++++++ xaxis.go | 5 +- yaxis.go | 28 +- 18 files changed, 1987 insertions(+), 85 deletions(-) create mode 100644 echarts.go create mode 100644 examples/funnel_chart/main.go create mode 100644 examples/pie_chart/main.go create mode 100644 examples/radar_chart/main.go create mode 100644 funnel_chart.go create mode 100644 pie_chart.go create mode 100644 radar_chart.go diff --git a/axis.go b/axis.go index d069c39..7b828d2 100644 --- a/axis.go +++ b/axis.go @@ -23,7 +23,10 @@ package charts import ( + "strings" + "github.com/golang/freetype/truetype" + "github.com/wcharczuk/go-chart/v2" ) type axisPainter struct { @@ -41,11 +44,15 @@ func NewAxisPainter(p *Painter, opt AxisOption) *axisPainter { type AxisOption struct { // The theme of chart Theme ColorPalette + // Formatter for y axis text value + Formatter string // 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 flag for show axis, set this to *false will hide axis + Show *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. @@ -74,6 +81,9 @@ func (a *axisPainter) Render() (Box, error) { opt := a.opt top := a.p theme := opt.Theme + if opt.Show != nil && !*opt.Show { + return BoxZero, nil + } strokeWidth := opt.StrokeWidth if strokeWidth == 0 { @@ -97,10 +107,15 @@ func (a *axisPainter) Render() (Box, error) { strokeColor = theme.GetAxisStrokeColor() } - tickCount := opt.SplitNumber - if tickCount == 0 { - tickCount = len(opt.Data) + data := opt.Data + formatter := opt.Formatter + if len(formatter) != 0 { + for index, text := range data { + data[index] = strings.ReplaceAll(formatter, "{value}", text) + } } + dataCount := len(data) + tickCount := dataCount boundaryGap := true if opt.BoundaryGap != nil && !*opt.BoundaryGap { @@ -118,8 +133,6 @@ func (a *axisPainter) Render() (Box, error) { labelPosition = PositionCenter } - // TODO 计算unit - unit := 1 // 如果小于0,则表示不处理 tickLength := getDefaultInt(opt.TickLength, 5) labelMargin := getDefaultInt(opt.LabelMargin, 5) @@ -133,7 +146,9 @@ func (a *axisPainter) Render() (Box, error) { } top.SetDrawingStyle(style).OverrideTextStyle(style) - textMaxWidth, textMaxHeight := top.MeasureTextMaxWidthHeight(opt.Data) + textMaxWidth, textMaxHeight := top.MeasureTextMaxWidthHeight(data) + textCount := ceilFloatToInt(float64(top.Width()) / float64(textMaxWidth)) + unit := ceilFloatToInt(float64(dataCount) / float64(chart.MaxInt(textCount, opt.SplitNumber))) width := 0 height := 0 @@ -226,7 +241,7 @@ func (a *axisPainter) Render() (Box, error) { Right: labelPaddingRight, })).MultiText(MultiTextOption{ Align: textAlign, - TextList: opt.Data, + TextList: data, Orient: orient, Unit: unit, Position: labelPosition, @@ -242,10 +257,7 @@ func (a *axisPainter) Render() (Box, error) { x0 = 0 x1 = top.Width() - p.Width() } - for index, y := range autoDivide(height, tickCount) { - if index == 0 { - continue - } + for _, y := range autoDivide(height, tickCount) { top.LineStroke([]Point{ { X: x0, diff --git a/bar_chart.go b/bar_chart.go index 8fae4df..8330542 100644 --- a/bar_chart.go +++ b/bar_chart.go @@ -95,14 +95,10 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B markPointPainter, markLinePainter, } - for i := range seriesList { - series := seriesList[i] + for index := range seriesList { + series := seriesList[index] yRange := result.axisRanges[series.AxisIndex] - index := series.index - if index == 0 { - index = i - } - seriesColor := theme.GetSeriesColor(index) + seriesColor := theme.GetSeriesColor(series.index) divideValues := xRange.AutoDivide() points := make([]Point, len(series.Data)) @@ -112,8 +108,8 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B } x := divideValues[j] x += margin - if i != 0 { - x += i * (barWidth + barMargin) + if index != 0 { + x += index * (barWidth + barMargin) } h := int(yRange.getHeight(item.Value)) @@ -151,7 +147,7 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B if distance == 0 { distance = 5 } - text := NewValueLabelFormater(seriesNames, series.Label.Formatter)(i, item.Value, -1) + text := NewValueLabelFormater(seriesNames, series.Label.Formatter)(index, item.Value, -1) labelStyle := Style{ FontColor: theme.GetTextColor(), FontSize: labelFontSize, diff --git a/chart_option.go b/chart_option.go index 6ca7cd7..0cea754 100644 --- a/chart_option.go +++ b/chart_option.go @@ -59,7 +59,7 @@ type ChartOption struct { // The series list SeriesList SeriesList // The radar indicator list - // RadarIndicators []RadarIndicator + RadarIndicators []RadarIndicator // The background color of chart BackgroundColor Color // The child charts diff --git a/charts.go b/charts.go index ae14c8d..51e247a 100644 --- a/charts.go +++ b/charts.go @@ -22,7 +22,10 @@ package charts -import "errors" +import ( + "errors" + "sort" +) const labelFontSize = 10 const defaultDotWidth = 2.0 @@ -140,16 +143,29 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e if containsInt(axisIndexList, series.AxisIndex) { continue } - axisIndexList = append(axisIndexList, series.index) + axisIndexList = append(axisIndexList, series.AxisIndex) } // 高度需要减去x轴的高度 rangeHeight := p.Height() - defaultXAxisHeight rangeWidthLeft := 0 rangeWidthRight := 0 + // 倒序 + sort.Sort(sort.Reverse(sort.IntSlice(axisIndexList))) + // 计算对应的axis range for _, index := range axisIndexList { + yAxisOption := YAxisOption{} + if len(opt.YAxisOptions) > index { + yAxisOption = opt.YAxisOptions[index] + } max, min := opt.SeriesList.GetMaxMin(index) + if yAxisOption.Min != nil { + min = *yAxisOption.Min + } + if yAxisOption.Max != nil { + max = *yAxisOption.Max + } r := NewRange(AxisRangeOption{ Min: min, Max: max, @@ -159,10 +175,7 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e DivideCount: defaultAxisDivideCount, }) result.axisRanges[index] = r - yAxisOption := YAxisOption{} - if len(opt.YAxisOptions) > index { - yAxisOption = opt.YAxisOptions[index] - } + if yAxisOption.Theme == nil { yAxisOption.Theme = opt.Theme } @@ -175,7 +188,16 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e } reverseStringSlice(yAxisOption.Data) // TODO生成其它位置既yAxis - yAxis := NewLeftYAxis(p, yAxisOption) + var yAxis *axisPainter + child := p.Child(PainterPaddingOption(Box{ + Left: rangeWidthLeft, + Right: rangeWidthRight, + })) + if index == 0 { + yAxis = NewLeftYAxis(child, yAxisOption) + } else { + yAxis = NewRightYAxis(child, yAxisOption) + } yAxisBox, err := yAxis.Render() if err != nil { return nil, err @@ -191,7 +213,8 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e opt.XAxis.Theme = opt.Theme } xAxis := NewBottomXAxis(p.Child(PainterPaddingOption(Box{ - Left: rangeWidthLeft, + Left: rangeWidthLeft, + Right: rangeWidthRight, })), opt.XAxis) _, err := xAxis.Render() if err != nil { @@ -219,7 +242,9 @@ func doRender(renderers ...Renderer) error { func Render(opt ChartOption) (*Painter, error) { opt.fillDefault() + isChild := true if opt.Parent == nil { + isChild = false p, err := NewPainter(PainterOptions{ Type: opt.Type, Width: opt.Width, @@ -231,21 +256,40 @@ func Render(opt ChartOption) (*Painter, error) { opt.Parent = p } p := opt.Parent - p.SetBackground(p.Width(), p.Height(), opt.BackgroundColor) + if !opt.Box.IsZero() { + p = p.Child(PainterBoxOption(opt.Box)) + } + if !isChild { + p.SetBackground(p.Width(), p.Height(), opt.BackgroundColor) + } seriesList := opt.SeriesList seriesList.init() + seriesCount := len(seriesList) + // line chart lineSeriesList := seriesList.Filter(ChartTypeLine) barSeriesList := seriesList.Filter(ChartTypeBar) horizontalBarSeriesList := seriesList.Filter(ChartTypeHorizontalBar) - if len(horizontalBarSeriesList) != 0 && len(horizontalBarSeriesList) != len(seriesList) { + pieSeriesList := seriesList.Filter(ChartTypePie) + radarSeriesList := seriesList.Filter(ChartTypeRadar) + funnelSeriesList := seriesList.Filter(ChartTypeFunnel) + + if len(horizontalBarSeriesList) != 0 && len(horizontalBarSeriesList) != seriesCount { return nil, errors.New("Horizontal bar can not mix other charts") } + if len(pieSeriesList) != 0 && len(pieSeriesList) != seriesCount { + return nil, errors.New("Pie can not mix other charts") + } + if len(radarSeriesList) != 0 && len(radarSeriesList) != seriesCount { + return nil, errors.New("Radar can not mix other charts") + } + if len(funnelSeriesList) != 0 && len(funnelSeriesList) != seriesCount { + return nil, errors.New("Funnel can not mix other charts") + } axisReversed := len(horizontalBarSeriesList) != 0 - - renderResult, err := defaultRender(p, defaultRenderOption{ + renderOpt := defaultRenderOption{ Theme: opt.theme, Padding: opt.Padding, SeriesList: opt.SeriesList, @@ -254,24 +298,28 @@ func Render(opt ChartOption) (*Painter, error) { TitleOption: opt.Title, LegendOption: opt.Legend, axisReversed: axisReversed, - }) + } + if isChild { + renderOpt.backgroundIsFilled = true + } + if len(pieSeriesList) != 0 || + len(radarSeriesList) != 0 || + len(funnelSeriesList) != 0 { + renderOpt.XAxis.Show = FalseFlag() + renderOpt.YAxisOptions = []YAxisOption{ + { + Show: FalseFlag(), + }, + } + } + + renderResult, err := defaultRender(p, renderOpt) if err != nil { return nil, err } handler := renderHandler{} - if len(lineSeriesList) != 0 { - handler.Add(func() error { - _, err := NewLineChart(p, LineChartOption{ - Theme: opt.theme, - Font: opt.font, - XAxis: opt.XAxis, - }).render(renderResult, lineSeriesList) - return err - }) - } - // bar chart if len(barSeriesList) != 0 { handler.Add(func() error { @@ -296,11 +344,65 @@ func Render(opt ChartOption) (*Painter, error) { }) } + // pie chart + if len(pieSeriesList) != 0 { + handler.Add(func() error { + _, err := NewPieChart(p, PieChartOption{ + Theme: opt.theme, + Font: opt.font, + }).render(renderResult, pieSeriesList) + return err + }) + } + + // line chart + if len(lineSeriesList) != 0 { + handler.Add(func() error { + _, err := NewLineChart(p, LineChartOption{ + Theme: opt.theme, + Font: opt.font, + XAxis: opt.XAxis, + }).render(renderResult, lineSeriesList) + return err + }) + } + + // radar chart + if len(radarSeriesList) != 0 { + handler.Add(func() error { + _, err := NewRadarChart(p, RadarChartOption{ + Theme: opt.theme, + Font: opt.font, + // 相应值 + RadarIndicators: opt.RadarIndicators, + }).render(renderResult, radarSeriesList) + return err + }) + } + + // funnel chart + if len(funnelSeriesList) != 0 { + handler.Add(func() error { + _, err := NewFunnelChart(p, FunnelChartOption{ + Theme: opt.theme, + Font: opt.font, + }).render(renderResult, funnelSeriesList) + return err + }) + } + err = handler.Do() if err != nil { return nil, err } + for _, item := range opt.Children { + item.Parent = p + _, err = Render(item) + if err != nil { + return nil, err + } + } return p, nil } diff --git a/echarts.go b/echarts.go new file mode 100644 index 0000000..ac28436 --- /dev/null +++ b/echarts.go @@ -0,0 +1,505 @@ +// 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" + "encoding/json" + "fmt" + "regexp" + "strconv" + + "github.com/wcharczuk/go-chart/v2" +) + +func convertToArray(data []byte) []byte { + data = bytes.TrimSpace(data) + if len(data) == 0 { + return nil + } + if data[0] != '[' { + data = []byte("[" + string(data) + "]") + } + return data +} + +type EChartsPosition string + +func (p *EChartsPosition) UnmarshalJSON(data []byte) error { + if len(data) == 0 { + return nil + } + if regexp.MustCompile(`^\d+`).Match(data) { + data = []byte(fmt.Sprintf(`"%s"`, string(data))) + } + s := (*string)(p) + return json.Unmarshal(data, s) +} + +type EChartStyle struct { + Color string `json:"color"` +} + +func (es *EChartStyle) ToStyle() chart.Style { + color := parseColor(es.Color) + return chart.Style{ + FillColor: color, + FontColor: color, + StrokeColor: color, + } +} + +type EChartsSeriesDataValue struct { + values []float64 +} + +func (value *EChartsSeriesDataValue) UnmarshalJSON(data []byte) error { + data = convertToArray(data) + return json.Unmarshal(data, &value.values) +} +func (value *EChartsSeriesDataValue) First() float64 { + if len(value.values) == 0 { + return 0 + } + return value.values[0] +} +func NewEChartsSeriesDataValue(values ...float64) EChartsSeriesDataValue { + return EChartsSeriesDataValue{ + values: values, + } +} + +type EChartsSeriesData struct { + Value EChartsSeriesDataValue `json:"value"` + Name string `json:"name"` + ItemStyle EChartStyle `json:"itemStyle"` +} +type _EChartsSeriesData EChartsSeriesData + +var numericRep = regexp.MustCompile(`^[-+]?[0-9]+(?:\.[0-9]+)?$`) + +func (es *EChartsSeriesData) UnmarshalJSON(data []byte) error { + data = bytes.TrimSpace(data) + if len(data) == 0 { + return nil + } + if numericRep.Match(data) { + v, err := strconv.ParseFloat(string(data), 64) + if err != nil { + return err + } + es.Value = EChartsSeriesDataValue{ + values: []float64{ + v, + }, + } + return nil + } + v := _EChartsSeriesData{} + err := json.Unmarshal(data, &v) + if err != nil { + return err + } + es.Name = v.Name + es.Value = v.Value + es.ItemStyle = v.ItemStyle + return nil +} + +type EChartsXAxisData struct { + BoundaryGap *bool `json:"boundaryGap"` + SplitNumber int `json:"splitNumber"` + Data []string `json:"data"` +} +type EChartsXAxis struct { + Data []EChartsXAxisData +} + +func (ex *EChartsXAxis) UnmarshalJSON(data []byte) error { + data = convertToArray(data) + if len(data) == 0 { + return nil + } + return json.Unmarshal(data, &ex.Data) +} + +type EChartsAxisLabel struct { + Formatter string `json:"formatter"` +} +type EChartsYAxisData struct { + Min *float64 `json:"min"` + Max *float64 `json:"max"` + AxisLabel EChartsAxisLabel `json:"axisLabel"` + AxisLine struct { + LineStyle struct { + Color string `json:"color"` + } `json:"lineStyle"` + } `json:"axisLine"` +} +type EChartsYAxis struct { + Data []EChartsYAxisData `json:"data"` +} + +func (ey *EChartsYAxis) UnmarshalJSON(data []byte) error { + data = convertToArray(data) + if len(data) == 0 { + return nil + } + return json.Unmarshal(data, &ey.Data) +} + +type EChartsPadding struct { + Box chart.Box +} + +func (eb *EChartsPadding) UnmarshalJSON(data []byte) error { + data = convertToArray(data) + if len(data) == 0 { + return nil + } + arr := make([]int, 0) + err := json.Unmarshal(data, &arr) + if err != nil { + return err + } + if len(arr) == 0 { + return nil + } + switch len(arr) { + case 1: + eb.Box = chart.Box{ + Left: arr[0], + Top: arr[0], + Bottom: arr[0], + Right: arr[0], + } + case 2: + eb.Box = chart.Box{ + Top: arr[0], + Bottom: arr[0], + Left: arr[1], + Right: arr[1], + } + default: + result := make([]int, 4) + copy(result, arr) + if len(arr) == 3 { + result[3] = result[1] + } + // 上右下左 + eb.Box = chart.Box{ + Top: result[0], + Right: result[1], + Bottom: result[2], + Left: result[3], + } + } + return nil +} + +type EChartsLabelOption struct { + Show bool `json:"show"` + Distance int `json:"distance"` + Color string `json:"color"` +} +type EChartsLegend struct { + Show *bool `json:"show"` + Data []string `json:"data"` + Align string `json:"align"` + Orient string `json:"orient"` + Padding EChartsPadding `json:"padding"` + Left EChartsPosition `json:"left"` + Top EChartsPosition `json:"top"` + TextStyle EChartsTextStyle `json:"textStyle"` +} + +type EChartsMarkData struct { + Type string `json:"type"` +} +type _EChartsMarkData EChartsMarkData + +func (emd *EChartsMarkData) UnmarshalJSON(data []byte) error { + data = bytes.TrimSpace(data) + if len(data) == 0 { + return nil + } + data = convertToArray(data) + ds := make([]*_EChartsMarkData, 0) + err := json.Unmarshal(data, &ds) + if err != nil { + return err + } + for _, d := range ds { + if d.Type != "" { + emd.Type = d.Type + } + } + return nil +} + +type EChartsMarkPoint struct { + SymbolSize int `json:"symbolSize"` + Data []EChartsMarkData `json:"data"` +} + +func (emp *EChartsMarkPoint) ToSeriesMarkPoint() SeriesMarkPoint { + sp := SeriesMarkPoint{ + SymbolSize: emp.SymbolSize, + } + if len(emp.Data) == 0 { + return sp + } + data := make([]SeriesMarkData, len(emp.Data)) + for index, item := range emp.Data { + data[index].Type = item.Type + } + sp.Data = data + return sp +} + +type EChartsMarkLine struct { + Data []EChartsMarkData `json:"data"` +} + +func (eml *EChartsMarkLine) ToSeriesMarkLine() SeriesMarkLine { + sl := SeriesMarkLine{} + if len(eml.Data) == 0 { + return sl + } + data := make([]SeriesMarkData, len(eml.Data)) + for index, item := range eml.Data { + data[index].Type = item.Type + } + sl.Data = data + return sl +} + +type EChartsSeries struct { + Data []EChartsSeriesData `json:"data"` + Name string `json:"name"` + Type string `json:"type"` + Radius string `json:"radius"` + YAxisIndex int `json:"yAxisIndex"` + ItemStyle EChartStyle `json:"itemStyle"` + // label的配置 + Label EChartsLabelOption `json:"label"` + MarkPoint EChartsMarkPoint `json:"markPoint"` + MarkLine EChartsMarkLine `json:"markLine"` + Max *float64 `json:"max"` + Min *float64 `json:"min"` +} +type EChartsSeriesList []EChartsSeries + +func (esList EChartsSeriesList) ToSeriesList() SeriesList { + seriesList := make(SeriesList, 0, len(esList)) + for _, item := range esList { + // 如果是pie,则每个子荐生成一个series + if item.Type == ChartTypePie { + for _, dataItem := range item.Data { + seriesList = append(seriesList, Series{ + Type: item.Type, + Name: dataItem.Name, + Label: SeriesLabel{ + Show: true, + }, + Radius: item.Radius, + Data: []SeriesData{ + { + Value: dataItem.Value.First(), + }, + }, + }) + } + continue + } + // 如果是radar或funnel + if item.Type == ChartTypeRadar || + item.Type == ChartTypeFunnel { + for _, dataItem := range item.Data { + seriesList = append(seriesList, Series{ + Name: dataItem.Name, + Type: item.Type, + Data: NewSeriesDataFromValues(dataItem.Value.values), + Max: item.Max, + Min: item.Min, + }) + } + continue + } + data := make([]SeriesData, len(item.Data)) + for j, dataItem := range item.Data { + data[j] = SeriesData{ + Value: dataItem.Value.First(), + Style: dataItem.ItemStyle.ToStyle(), + } + } + seriesList = append(seriesList, Series{ + Type: item.Type, + Data: data, + AxisIndex: item.YAxisIndex, + Style: item.ItemStyle.ToStyle(), + Label: SeriesLabel{ + Color: parseColor(item.Label.Color), + Show: item.Label.Show, + Distance: item.Label.Distance, + }, + Name: item.Name, + MarkPoint: item.MarkPoint.ToSeriesMarkPoint(), + MarkLine: item.MarkLine.ToSeriesMarkLine(), + }) + } + return seriesList +} + +type EChartsTextStyle struct { + Color string `json:"color"` + FontFamily string `json:"fontFamily"` + FontSize float64 `json:"fontSize"` +} + +func (et *EChartsTextStyle) ToStyle() chart.Style { + s := chart.Style{ + FontSize: et.FontSize, + FontColor: parseColor(et.Color), + } + if et.FontFamily != "" { + s.Font, _ = GetFont(et.FontFamily) + } + return s +} + +type EChartsOption struct { + Type string `json:"type"` + Theme string `json:"theme"` + FontFamily string `json:"fontFamily"` + Padding EChartsPadding `json:"padding"` + Box chart.Box `json:"box"` + Width int `json:"width"` + Height int `json:"height"` + Title struct { + Text string `json:"text"` + Subtext string `json:"subtext"` + Left EChartsPosition `json:"left"` + Top EChartsPosition `json:"top"` + TextStyle EChartsTextStyle `json:"textStyle"` + SubtextStyle EChartsTextStyle `json:"subtextStyle"` + } `json:"title"` + XAxis EChartsXAxis `json:"xAxis"` + YAxis EChartsYAxis `json:"yAxis"` + Legend EChartsLegend `json:"legend"` + Radar struct { + Indicator []RadarIndicator `json:"indicator"` + } `json:"radar"` + Series EChartsSeriesList `json:"series"` + Children []EChartsOption `json:"children"` +} + +func (eo *EChartsOption) ToOption() ChartOption { + fontFamily := eo.FontFamily + if len(fontFamily) == 0 { + fontFamily = eo.Title.TextStyle.FontFamily + } + titleTextStyle := eo.Title.TextStyle.ToStyle() + titleSubtextStyle := eo.Title.SubtextStyle.ToStyle() + legendTextStyle := eo.Legend.TextStyle.ToStyle() + o := ChartOption{ + Type: eo.Type, + FontFamily: fontFamily, + Theme: eo.Theme, + Title: TitleOption{ + Text: eo.Title.Text, + Subtext: eo.Title.Subtext, + FontColor: titleTextStyle.FontColor, + FontSize: titleTextStyle.FontSize, + SubtextFontSize: titleSubtextStyle.FontSize, + SubtextFontColor: titleSubtextStyle.FontColor, + Left: string(eo.Title.Left), + Top: string(eo.Title.Top), + }, + Legend: LegendOption{ + Show: eo.Legend.Show, + FontSize: legendTextStyle.FontSize, + FontColor: legendTextStyle.FontColor, + Data: eo.Legend.Data, + Left: string(eo.Legend.Left), + Top: string(eo.Legend.Top), + Align: eo.Legend.Align, + Orient: eo.Legend.Orient, + }, + RadarIndicators: eo.Radar.Indicator, + Width: eo.Width, + Height: eo.Height, + Padding: eo.Padding.Box, + Box: eo.Box, + SeriesList: eo.Series.ToSeriesList(), + } + if len(eo.XAxis.Data) != 0 { + xAxisData := eo.XAxis.Data[0] + o.XAxis = XAxisOption{ + BoundaryGap: xAxisData.BoundaryGap, + Data: xAxisData.Data, + SplitNumber: xAxisData.SplitNumber, + } + } + yAxisOptions := make([]YAxisOption, len(eo.YAxis.Data)) + for index, item := range eo.YAxis.Data { + yAxisOptions[index] = YAxisOption{ + Min: item.Min, + Max: item.Max, + Formatter: item.AxisLabel.Formatter, + Color: parseColor(item.AxisLine.LineStyle.Color), + } + } + o.YAxisOptions = yAxisOptions + + if len(eo.Children) != 0 { + o.Children = make([]ChartOption, len(eo.Children)) + for index, item := range eo.Children { + o.Children[index] = item.ToOption() + } + } + return o +} + +func renderEcharts(options, outputType string) ([]byte, error) { + o := EChartsOption{} + err := json.Unmarshal([]byte(options), &o) + if err != nil { + return nil, err + } + opt := o.ToOption() + opt.Type = outputType + d, err := Render(opt) + if err != nil { + return nil, err + } + return d.Bytes() +} + +func RenderEChartsToPNG(options string) ([]byte, error) { + return renderEcharts(options, "png") +} + +func RenderEChartsToSVG(options string) ([]byte, error) { + return renderEcharts(options, "svg") +} diff --git a/examples/charts/main.go b/examples/charts/main.go index c7986a4..3a625f7 100644 --- a/examples/charts/main.go +++ b/examples/charts/main.go @@ -83,13 +83,13 @@ func handler(w http.ResponseWriter, req *http.Request, chartOptions []charts.Cha } bytesList = append(bytesList, buf) } - // for _, opt := range echartsOptions { - // buf, err := charts.RenderEChartsToSVG(opt) - // if err != nil { - // panic(err) - // } - // bytesList = append(bytesList, buf) - // } + for _, opt := range echartsOptions { + buf, err := charts.RenderEChartsToSVG(opt) + if err != nil { + panic(err) + } + bytesList = append(bytesList, buf) + } data := bytes.ReplaceAll([]byte(html), []byte("{{body}}"), bytes.Join(bytesList, []byte(""))) w.Header().Set("Content-Type", "text/html") @@ -333,6 +333,7 @@ func indexHandler(w http.ResponseWriter, req *http.Request) { }, }, }, + // 柱状图+标记 { Title: charts.TitleOption{ Text: "Rainfall vs Evaporation", @@ -413,6 +414,342 @@ func indexHandler(w http.ResponseWriter, req *http.Request) { }, }, }, + // 双Y轴示例 + { + Title: charts.TitleOption{ + Text: "Temperature", + }, + XAxis: charts.NewXAxisOption([]string{ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + }), + Legend: charts.NewLegendOption([]string{ + "Evaporation", + "Precipitation", + "Temperature", + }), + YAxisOptions: []charts.YAxisOption{ + { + Formatter: "{value}ml", + Color: charts.Color{ + R: 84, + G: 112, + B: 198, + A: 255, + }, + }, + { + Formatter: "{value}°C", + Color: charts.Color{ + R: 250, + G: 200, + B: 88, + A: 255, + }, + }, + }, + SeriesList: []charts.Series{ + { + Type: charts.ChartTypeBar, + Data: charts.NewSeriesDataFromValues([]float64{ + 2.0, + 4.9, + 7.0, + 23.2, + 25.6, + 76.7, + 135.6, + 162.2, + 32.6, + 20.0, + 6.4, + 3.3, + }), + }, + { + Type: charts.ChartTypeBar, + Data: charts.NewSeriesDataFromValues([]float64{ + 2.6, + 5.9, + 9.0, + 26.4, + 28.7, + 70.7, + 175.6, + 182.2, + 48.7, + 18.8, + 6.0, + 2.3, + }), + }, + { + Data: charts.NewSeriesDataFromValues([]float64{ + 2.0, + 2.2, + 3.3, + 4.5, + 6.3, + 10.2, + 20.3, + 23.4, + 23.0, + 16.5, + 12.0, + 6.2, + }), + AxisIndex: 1, + }, + }, + }, + // 饼图 + { + Title: charts.TitleOption{ + Text: "Referer of a Website", + Subtext: "Fake Data", + Left: charts.PositionCenter, + }, + Legend: charts.LegendOption{ + Orient: charts.OrientVertical, + Data: []string{ + "Search Engine", + "Direct", + "Email", + "Union Ads", + "Video Ads", + }, + Left: charts.PositionLeft, + }, + SeriesList: charts.NewPieSeriesList([]float64{ + 1048, + 735, + 580, + 484, + 300, + }, charts.PieSeriesOption{ + Label: charts.SeriesLabel{ + Show: true, + }, + Radius: "35%", + }), + }, + // 雷达图 + { + Title: charts.TitleOption{ + Text: "Basic Radar Chart", + }, + Legend: charts.NewLegendOption([]string{ + "Allocated Budget", + "Actual Spending", + }), + RadarIndicators: []charts.RadarIndicator{ + { + Name: "Sales", + Max: 6500, + }, + { + Name: "Administration", + Max: 16000, + }, + { + Name: "Information Technology", + Max: 30000, + }, + { + Name: "Customer Support", + Max: 38000, + }, + { + Name: "Development", + Max: 52000, + }, + { + Name: "Marketing", + Max: 25000, + }, + }, + SeriesList: charts.SeriesList{ + { + Type: charts.ChartTypeRadar, + Data: charts.NewSeriesDataFromValues([]float64{ + 4200, + 3000, + 20000, + 35000, + 50000, + 18000, + }), + }, + { + Type: charts.ChartTypeRadar, + Data: charts.NewSeriesDataFromValues([]float64{ + 5000, + 14000, + 28000, + 26000, + 42000, + 21000, + }), + }, + }, + }, + // 漏斗图 + { + Title: charts.TitleOption{ + Text: "Funnel", + }, + Legend: charts.NewLegendOption([]string{ + "Show", + "Click", + "Visit", + "Inquiry", + "Order", + }), + SeriesList: []charts.Series{ + { + Type: charts.ChartTypeFunnel, + Name: "Show", + Data: charts.NewSeriesDataFromValues([]float64{ + 100, + }), + }, + { + Type: charts.ChartTypeFunnel, + Name: "Click", + Data: charts.NewSeriesDataFromValues([]float64{ + 80, + }), + }, + { + Type: charts.ChartTypeFunnel, + Name: "Visit", + Data: charts.NewSeriesDataFromValues([]float64{ + 60, + }), + }, + { + Type: charts.ChartTypeFunnel, + Name: "Inquiry", + Data: charts.NewSeriesDataFromValues([]float64{ + 40, + }), + }, + { + Type: charts.ChartTypeFunnel, + Name: "Order", + Data: charts.NewSeriesDataFromValues([]float64{ + 20, + }), + }, + }, + }, + // 多图展示 + { + Legend: charts.LegendOption{ + Top: "-90", + Data: []string{ + "Milk Tea", + "Matcha Latte", + "Cheese Cocoa", + "Walnut Brownie", + }, + }, + Padding: charts.Box{ + Top: 100, + Right: 10, + Bottom: 10, + Left: 10, + }, + XAxis: charts.NewXAxisOption([]string{ + "2012", + "2013", + "2014", + "2015", + "2016", + "2017", + }), + YAxisOptions: []charts.YAxisOption{ + { + + Min: charts.NewFloatPoint(0), + Max: charts.NewFloatPoint(90), + }, + }, + SeriesList: []charts.Series{ + charts.NewSeriesFromValues([]float64{ + 56.5, + 82.1, + 88.7, + 70.1, + 53.4, + 85.1, + }), + charts.NewSeriesFromValues([]float64{ + 51.1, + 51.4, + 55.1, + 53.3, + 73.8, + 68.7, + }), + charts.NewSeriesFromValues([]float64{ + 40.1, + 62.2, + 69.5, + 36.4, + 45.2, + 32.5, + }, charts.ChartTypeBar), + charts.NewSeriesFromValues([]float64{ + 25.2, + 37.1, + 41.2, + 18, + 33.9, + 49.1, + }, charts.ChartTypeBar), + }, + Children: []charts.ChartOption{ + { + Legend: charts.LegendOption{ + Show: charts.FalseFlag(), + Data: []string{ + "Milk Tea", + "Matcha Latte", + "Cheese Cocoa", + "Walnut Brownie", + }, + }, + Box: charts.Box{ + Top: 20, + Left: 400, + Right: 500, + Bottom: 120, + }, + SeriesList: charts.NewPieSeriesList([]float64{ + 435.9, + 354.3, + 285.9, + 204.5, + }, charts.PieSeriesOption{ + Label: charts.SeriesLabel{ + Show: true, + }, + Radius: "35%", + }), + }, + }, + }, } handler(w, req, chartOptions, nil) } @@ -879,12 +1216,7 @@ func echartsHandler(w http.ResponseWriter, req *http.Request) { 23.2, 25.6, 76.7, - 135.6, - 162.2, - 32.6, - 20, - 6.4, - 3.3 + 135.6 ] }, { @@ -898,12 +1230,7 @@ func echartsHandler(w http.ResponseWriter, req *http.Request) { 26.4, 28.7, 70.7, - 175.6, - 182.2, - 48.7, - 18.8, - 6, - 2.3 + 175.6 ] }, { @@ -918,12 +1245,7 @@ func echartsHandler(w http.ResponseWriter, req *http.Request) { 4.5, 6.3, 10.2, - 20.3, - 23.4, - 23, - 16.5, - 12, - 6.2 + 20.3 ] } ] diff --git a/examples/funnel_chart/main.go b/examples/funnel_chart/main.go new file mode 100644 index 0000000..eb753fd --- /dev/null +++ b/examples/funnel_chart/main.go @@ -0,0 +1,97 @@ +package main + +import ( + "io/ioutil" + "os" + "path/filepath" + + "github.com/vicanso/go-charts" +) + +func writeFile(buf []byte) error { + tmpPath := "./tmp" + err := os.MkdirAll(tmpPath, 0700) + if err != nil { + return err + } + + file := filepath.Join(tmpPath, "funnel-chart.png") + err = ioutil.WriteFile(file, buf, 0600) + if err != nil { + return err + } + return nil +} + +func main() { + p, err := charts.NewPainter(charts.PainterOptions{ + Width: 800, + Height: 600, + Type: charts.ChartOutputPNG, + }) + if err != nil { + panic(err) + } + _, err = charts.NewFunnelChart(p, charts.FunnelChartOption{ + Title: charts.TitleOption{ + Text: "Funnel", + }, + Legend: charts.NewLegendOption([]string{ + "Show", + "Click", + "Visit", + "Inquiry", + "Order", + }), + SeriesList: []charts.Series{ + + { + Type: charts.ChartTypeFunnel, + Name: "Show", + Data: charts.NewSeriesDataFromValues([]float64{ + 100, + }), + }, + { + Type: charts.ChartTypeFunnel, + Name: "Click", + Data: charts.NewSeriesDataFromValues([]float64{ + 80, + }), + }, + { + Type: charts.ChartTypeFunnel, + Name: "Visit", + Data: charts.NewSeriesDataFromValues([]float64{ + 60, + }), + }, + { + Type: charts.ChartTypeFunnel, + Name: "Inquiry", + Data: charts.NewSeriesDataFromValues([]float64{ + 40, + }), + }, + { + Type: charts.ChartTypeFunnel, + Name: "Order", + Data: charts.NewSeriesDataFromValues([]float64{ + 20, + }), + }, + }, + }).Render() + if err != nil { + panic(err) + } + + buf, err := p.Bytes() + if err != nil { + panic(err) + } + err = writeFile(buf) + if err != nil { + panic(err) + } +} diff --git a/examples/pie_chart/main.go b/examples/pie_chart/main.go new file mode 100644 index 0000000..e69bf60 --- /dev/null +++ b/examples/pie_chart/main.go @@ -0,0 +1,83 @@ +package main + +import ( + "io/ioutil" + "os" + "path/filepath" + + "github.com/vicanso/go-charts" +) + +func writeFile(buf []byte) error { + tmpPath := "./tmp" + err := os.MkdirAll(tmpPath, 0700) + if err != nil { + return err + } + + file := filepath.Join(tmpPath, "pie-chart.png") + err = ioutil.WriteFile(file, buf, 0600) + if err != nil { + return err + } + return nil +} + +func main() { + p, err := charts.NewPainter(charts.PainterOptions{ + Width: 800, + Height: 600, + Type: charts.ChartOutputPNG, + }) + if err != nil { + panic(err) + } + _, err = charts.NewPieChart(p, charts.PieChartOption{ + Title: charts.TitleOption{ + Text: "Rainfall vs Evaporation", + Subtext: "Fake Data", + Left: charts.PositionCenter, + }, + Padding: charts.Box{ + Top: 20, + Right: 20, + Bottom: 20, + Left: 20, + }, + Legend: charts.LegendOption{ + Orient: charts.OrientVertical, + Data: []string{ + "Search Engine", + "Direct", + "Email", + "Union Ads", + "Video Ads", + }, + Left: charts.PositionLeft, + }, + SeriesList: charts.NewPieSeriesList([]float64{ + 1048, + 735, + 580, + 484, + 300, + }, charts.PieSeriesOption{ + Label: charts.SeriesLabel{ + Show: true, + }, + Radius: "35%", + }), + }).Render() + if err != nil { + panic(err) + } + + buf, err := p.Bytes() + if err != nil { + panic(err) + } + err = writeFile(buf) + if err != nil { + panic(err) + } +} diff --git a/examples/radar_chart/main.go b/examples/radar_chart/main.go new file mode 100644 index 0000000..077fa48 --- /dev/null +++ b/examples/radar_chart/main.go @@ -0,0 +1,112 @@ +package main + +import ( + "io/ioutil" + "os" + "path/filepath" + + "github.com/vicanso/go-charts" +) + +func writeFile(buf []byte) error { + tmpPath := "./tmp" + err := os.MkdirAll(tmpPath, 0700) + if err != nil { + return err + } + + file := filepath.Join(tmpPath, "radar-chart.png") + err = ioutil.WriteFile(file, buf, 0600) + if err != nil { + return err + } + return nil +} + +func main() { + p, err := charts.NewPainter(charts.PainterOptions{ + Width: 800, + Height: 600, + Type: charts.ChartOutputPNG, + }) + if err != nil { + panic(err) + } + _, err = charts.NewRadarChart(p, charts.RadarChartOption{ + Padding: charts.Box{ + Left: 10, + Top: 10, + Right: 10, + Bottom: 10, + }, + Title: charts.TitleOption{ + Text: "Basic Radar Chart", + }, + Legend: charts.NewLegendOption([]string{ + "Allocated Budget", + "Actual Spending", + }), + RadarIndicators: []charts.RadarIndicator{ + { + Name: "Sales", + Max: 6500, + }, + { + Name: "Administration", + Max: 16000, + }, + { + Name: "Information Technology", + Max: 30000, + }, + { + Name: "Customer Support", + Max: 38000, + }, + { + Name: "Development", + Max: 52000, + }, + { + Name: "Marketing", + Max: 25000, + }, + }, + SeriesList: charts.SeriesList{ + { + Type: charts.ChartTypeRadar, + Data: charts.NewSeriesDataFromValues([]float64{ + 4200, + 3000, + 20000, + 35000, + 50000, + 18000, + }), + }, + { + Type: charts.ChartTypeRadar, + Data: charts.NewSeriesDataFromValues([]float64{ + 5000, + 14000, + 28000, + 26000, + 42000, + 21000, + }), + }, + }, + }).Render() + if err != nil { + panic(err) + } + + buf, err := p.Bytes() + if err != nil { + panic(err) + } + err = writeFile(buf) + if err != nil { + panic(err) + } +} diff --git a/funnel_chart.go b/funnel_chart.go new file mode 100644 index 0000000..c8457dd --- /dev/null +++ b/funnel_chart.go @@ -0,0 +1,172 @@ +// 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 ( + "fmt" + + "github.com/dustin/go-humanize" + "github.com/golang/freetype/truetype" +) + +type funnelChart struct { + p *Painter + opt *FunnelChartOption +} + +func NewFunnelChart(p *Painter, opt FunnelChartOption) *funnelChart { + if opt.Theme == nil { + opt.Theme = NewTheme("") + } + return &funnelChart{ + p: p, + opt: &opt, + } +} + +type FunnelChartOption struct { + Theme ColorPalette + // The font size + Font *truetype.Font + // The data series list + SeriesList SeriesList + // The padding of line chart + Padding Box + // The option of title + Title TitleOption + // The legend option + Legend LegendOption +} + +func (f *funnelChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) { + opt := f.opt + seriesPainter := result.seriesPainter + max := seriesList[0].Data[0].Value + min := float64(0) + for _, item := range seriesList { + if item.Max != nil { + max = *item.Max + } + if item.Min != nil { + min = *item.Min + } + } + theme := opt.Theme + gap := 2 + height := seriesPainter.Height() + width := seriesPainter.Width() + count := len(seriesList) + + h := (height - gap*(count-1)) / count + + y := 0 + widthList := make([]int, len(seriesList)) + textList := make([]string, len(seriesList)) + for index, item := range seriesList { + value := item.Data[0].Value + widthPercent := (value - min) / (max - min) + w := int(widthPercent * float64(width)) + widthList[index] = w + p := humanize.CommafWithDigits(value/max*100, 2) + "%" + textList[index] = fmt.Sprintf("%s(%s)", item.Name, p) + } + + for index, w := range widthList { + series := seriesList[index] + nextWidth := 0 + if index+1 < len(widthList) { + nextWidth = widthList[index+1] + } + topStartX := (width - w) >> 1 + topEndX := topStartX + w + bottomStartX := (width - nextWidth) >> 1 + bottomEndX := bottomStartX + nextWidth + points := []Point{ + { + X: topStartX, + Y: y, + }, + { + X: topEndX, + Y: y, + }, + { + X: bottomEndX, + Y: y + h, + }, + { + X: bottomStartX, + Y: y + h, + }, + { + X: topStartX, + Y: y, + }, + } + color := theme.GetSeriesColor(series.index) + + seriesPainter.OverrideDrawingStyle(Style{ + FillColor: color, + }).FillArea(points) + + // 文本 + text := textList[index] + seriesPainter.OverrideTextStyle(Style{ + FontColor: theme.GetTextColor(), + FontSize: labelFontSize, + Font: opt.Font, + }) + textBox := seriesPainter.MeasureText(text) + textX := width>>1 - textBox.Width()>>1 + textY := y + h>>1 + seriesPainter.Text(text, textX, textY) + y += (h + gap) + } + + return f.p.box, nil +} + +func (f *funnelChart) Render() (Box, error) { + p := f.p + opt := f.opt + renderResult, err := defaultRender(p, defaultRenderOption{ + Theme: opt.Theme, + Padding: opt.Padding, + SeriesList: opt.SeriesList, + XAxis: XAxisOption{ + Show: FalseFlag(), + }, + YAxisOptions: []YAxisOption{ + { + Show: FalseFlag(), + }, + }, + TitleOption: opt.Title, + LegendOption: opt.Legend, + }) + if err != nil { + return BoxZero, err + } + seriesList := opt.SeriesList.Filter(ChartTypeFunnel) + return f.render(renderResult, seriesList) +} diff --git a/horizontal_bar_chart.go b/horizontal_bar_chart.go index 87ca9ae..c98d688 100644 --- a/horizontal_bar_chart.go +++ b/horizontal_bar_chart.go @@ -92,13 +92,9 @@ func (h *horizontalBarChart) render(result *defaultRenderResult, seriesList Seri Size: seriesPainter.Width(), }) - for i := range seriesList { - series := seriesList[i] - index := series.index - if index == 0 { - index = i - } - seriesColor := theme.GetSeriesColor(index) + for index := range seriesList { + series := seriesList[index] + seriesColor := theme.GetSeriesColor(series.index) divideValues := yRange.AutoDivide() for j, item := range series.Data { if j >= yRange.divideCount { @@ -108,8 +104,8 @@ func (h *horizontalBarChart) render(result *defaultRenderResult, seriesList Seri j = yRange.divideCount - j - 1 y := divideValues[j] y += margin - if i != 0 { - y += i * (barHeight + barMargin) + if index != 0 { + y += index * (barHeight + barMargin) } w := int(xRange.getHeight(item.Value)) diff --git a/legend.go b/legend.go index 65793c9..cf8d417 100644 --- a/legend.go +++ b/legend.go @@ -56,6 +56,8 @@ type LegendOption struct { FontSize float64 // FontColor color of legend text FontColor Color + // The flag for show legend, set this to *false will hide legend + Show *bool } func NewLegendOption(labels []string, left ...string) LegendOption { @@ -68,6 +70,17 @@ func NewLegendOption(labels []string, left ...string) LegendOption { return opt } +func (opt *LegendOption) IsEmpty() bool { + isEmpty := true + for _, v := range opt.Data { + if v != "" { + isEmpty = false + break + } + } + return isEmpty +} + func NewLegendPainter(p *Painter, opt LegendOption) *legendPainter { return &legendPainter{ p: p, @@ -78,6 +91,10 @@ func NewLegendPainter(p *Painter, opt LegendOption) *legendPainter { func (l *legendPainter) Render() (Box, error) { opt := l.opt theme := opt.Theme + if opt.IsEmpty() || + (opt.Show != nil && !*opt.Show) { + return BoxZero, nil + } if theme == nil { theme = l.p.theme } @@ -90,7 +107,9 @@ func (l *legendPainter) Render() (Box, error) { if opt.Left == "" { opt.Left = PositionCenter } - p := l.p + p := l.p.Child(PainterPaddingOption(Box{ + Top: 5, + })) p.SetTextStyle(Style{ FontSize: opt.FontSize, FontColor: opt.FontColor, diff --git a/line_chart.go b/line_chart.go index 47a497f..c505a91 100644 --- a/line_chart.go +++ b/line_chart.go @@ -93,7 +93,7 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) ( } for index := range seriesList { series := seriesList[index] - seriesColor := opt.Theme.GetSeriesColor(index) + seriesColor := opt.Theme.GetSeriesColor(series.index) drawingStyle := Style{ StrokeColor: seriesColor, StrokeWidth: defaultStrokeWidth, diff --git a/painter.go b/painter.go index fff6ca7..5a8dd89 100644 --- a/painter.go +++ b/painter.go @@ -628,6 +628,9 @@ func (p *Painter) MultiText(opt MultiTextOption) *Painter { values = autoDivide(width, count) } for index, text := range opt.TextList { + if index%opt.Unit != 0 { + continue + } box := p.MeasureText(text) start := values[index] if positionCenter { diff --git a/pie_chart.go b/pie_chart.go new file mode 100644 index 0000000..c5a2ff2 --- /dev/null +++ b/pie_chart.go @@ -0,0 +1,211 @@ +// 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 ( + "errors" + "math" + + "github.com/golang/freetype/truetype" + "github.com/wcharczuk/go-chart/v2" +) + +type pieChart struct { + p *Painter + opt *PieChartOption +} + +type PieChartOption struct { + Theme ColorPalette + // The font size + Font *truetype.Font + // The data series list + SeriesList SeriesList + // The padding of line chart + Padding Box + // The option of title + Title TitleOption + // The legend option + Legend LegendOption + // background is filled + backgroundIsFilled bool +} + +func NewPieChart(p *Painter, opt PieChartOption) *pieChart { + if opt.Theme == nil { + opt.Theme = NewTheme("") + } + return &pieChart{ + p: p, + opt: &opt, + } +} + +func (p *pieChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) { + opt := p.opt + values := make([]float64, len(seriesList)) + total := float64(0) + radiusValue := "" + for index, series := range seriesList { + if len(series.Radius) != 0 { + radiusValue = series.Radius + } + value := float64(0) + for _, item := range series.Data { + value += item.Value + } + values[index] = value + total += value + } + if total <= 0 { + return BoxZero, errors.New("The sum value of pie chart should gt 0") + } + seriesPainter := result.seriesPainter + cx := seriesPainter.Width() >> 1 + cy := seriesPainter.Height() >> 1 + + diameter := chart.MinInt(seriesPainter.Width(), seriesPainter.Height()) + radius := getRadius(float64(diameter), radiusValue) + + labelLineWidth := 15 + if radius < 50 { + labelLineWidth = 10 + } + labelRadius := radius + float64(labelLineWidth) + seriesNames := opt.Legend.Data + if len(seriesNames) == 0 { + seriesNames = seriesList.Names() + } + theme := opt.Theme + if len(values) == 1 { + seriesPainter.OverrideDrawingStyle(Style{ + StrokeWidth: 1, + StrokeColor: theme.GetSeriesColor(0), + FillColor: theme.GetSeriesColor(0), + }) + seriesPainter.MoveTo(cx, cy). + Circle(radius, cx, cy) + } else { + currentValue := float64(0) + prevEndX := 0 + prevEndY := 0 + for index, v := range values { + seriesPainter.OverrideDrawingStyle(Style{ + StrokeWidth: 1, + StrokeColor: theme.GetSeriesColor(index), + FillColor: theme.GetSeriesColor(index), + }) + seriesPainter.MoveTo(cx, cy) + start := chart.PercentToRadians(currentValue/total) - math.Pi/2 + currentValue += v + percent := (v / total) + delta := chart.PercentToRadians(percent) + seriesPainter.ArcTo(cx, cy, radius, radius, start, delta). + LineTo(cx, cy). + Close(). + FillStroke() + + series := seriesList[index] + // 是否显示label + showLabel := series.Label.Show + if !showLabel { + continue + } + + // label的角度为饼块中间 + angle := start + delta/2 + startx := cx + int(radius*math.Cos(angle)) + starty := cy + int(radius*math.Sin(angle)) + + endx := cx + int(labelRadius*math.Cos(angle)) + endy := cy + int(labelRadius*math.Sin(angle)) + // 计算是否有重叠,如果有则调整y坐标位置 + if index != 0 && + math.Abs(float64(endx-prevEndX)) < labelFontSize && + math.Abs(float64(endy-prevEndY)) < labelFontSize { + endy -= (labelFontSize << 1) + } + prevEndX = endx + prevEndY = endy + + seriesPainter.MoveTo(startx, starty) + seriesPainter.LineTo(endx, endy) + offset := labelLineWidth + if endx < cx { + offset *= -1 + } + seriesPainter.MoveTo(endx, endy) + endx += offset + seriesPainter.LineTo(endx, endy) + seriesPainter.Stroke() + + textStyle := Style{ + FontColor: theme.GetTextColor(), + FontSize: labelFontSize, + Font: opt.Font, + } + if !series.Label.Color.IsZero() { + textStyle.FontColor = series.Label.Color + } + seriesPainter.OverrideTextStyle(textStyle) + text := NewPieLabelFormatter(seriesNames, series.Label.Formatter)(index, v, percent) + textBox := seriesPainter.MeasureText(text) + textMargin := 3 + x := endx + textMargin + y := endy + textBox.Height()>>1 - 1 + if offset < 0 { + textWidth := textBox.Width() + x = endx - textWidth - textMargin + } + seriesPainter.Text(text, x, y) + } + } + + return p.p.box, nil +} + +func (p *pieChart) Render() (Box, error) { + opt := p.opt + + renderResult, err := defaultRender(p.p, defaultRenderOption{ + Theme: opt.Theme, + Padding: opt.Padding, + SeriesList: opt.SeriesList, + XAxis: XAxisOption{ + Show: FalseFlag(), + }, + YAxisOptions: []YAxisOption{ + { + Show: FalseFlag(), + }, + }, + TitleOption: opt.Title, + LegendOption: opt.Legend, + backgroundIsFilled: opt.backgroundIsFilled, + }) + if err != nil { + return BoxZero, err + } + seriesList := opt.SeriesList.Filter(ChartTypePie) + return p.render(renderResult, seriesList) +} diff --git a/radar_chart.go b/radar_chart.go new file mode 100644 index 0000000..dc93ca8 --- /dev/null +++ b/radar_chart.go @@ -0,0 +1,245 @@ +// 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 ( + "errors" + + "github.com/golang/freetype/truetype" + "github.com/wcharczuk/go-chart/v2" + "github.com/wcharczuk/go-chart/v2/drawing" +) + +type radarChart struct { + p *Painter + opt *RadarChartOption +} + +type RadarIndicator struct { + // Indicator's name + Name string + // The maximum value of indicator + Max float64 + // The minimum value of indicator + Min float64 +} + +type RadarChartOption struct { + Theme ColorPalette + // The font size + Font *truetype.Font + // The data series list + SeriesList SeriesList + // The padding of line chart + Padding Box + // The option of title + Title TitleOption + // The legend option + Legend LegendOption + // The radar indicator list + RadarIndicators []RadarIndicator + // background is filled + backgroundIsFilled bool +} + +func NewRadarChart(p *Painter, opt RadarChartOption) *radarChart { + if opt.Theme == nil { + opt.Theme = NewTheme("") + } + return &radarChart{ + p: p, + opt: &opt, + } +} + +func (r *radarChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) { + opt := r.opt + indicators := opt.RadarIndicators + sides := len(indicators) + if sides < 3 { + return BoxZero, errors.New("The count of indicator should be >= 3") + } + maxValues := make([]float64, len(indicators)) + for _, series := range seriesList { + for index, item := range series.Data { + if index < len(maxValues) && item.Value > maxValues[index] { + maxValues[index] = item.Value + } + } + } + for index, indicator := range indicators { + if indicator.Max <= 0 { + indicators[index].Max = maxValues[index] + } + } + + radiusValue := "" + for _, series := range seriesList { + if len(series.Radius) != 0 { + radiusValue = series.Radius + } + } + + seriesPainter := result.seriesPainter + theme := opt.Theme + + cx := seriesPainter.Width() >> 1 + cy := seriesPainter.Height() >> 1 + diameter := chart.MinInt(seriesPainter.Width(), seriesPainter.Height()) + radius := getRadius(float64(diameter), radiusValue) + + divideCount := 5 + divideRadius := float64(int(radius / float64(divideCount))) + radius = divideRadius * float64(divideCount) + + seriesPainter.OverrideDrawingStyle(Style{ + StrokeColor: theme.GetAxisSplitLineColor(), + StrokeWidth: 1, + }) + center := Point{ + X: cx, + Y: cy, + } + for i := 0; i < divideCount; i++ { + seriesPainter.Polygon(center, divideRadius*float64(i+1), sides) + } + points := getPolygonPoints(center, radius, sides) + for _, p := range points { + seriesPainter.MoveTo(center.X, center.Y) + seriesPainter.LineTo(p.X, p.Y) + seriesPainter.Stroke() + } + seriesPainter.OverrideTextStyle(Style{ + FontColor: theme.GetTextColor(), + FontSize: labelFontSize, + Font: opt.Font, + }) + offset := 5 + // 文本生成 + for index, p := range points { + name := indicators[index].Name + b := seriesPainter.MeasureText(name) + isXCenter := p.X == center.X + isYCenter := p.Y == center.Y + isRight := p.X > center.X + isLeft := p.X < center.X + isTop := p.Y < center.Y + isBottom := p.Y > center.Y + x := p.X + y := p.Y + if isXCenter { + x -= b.Width() >> 1 + if isTop { + y -= b.Height() + } else { + y += b.Height() + } + } + if isYCenter { + y += b.Height() >> 1 + } + if isTop { + y += offset + } + if isBottom { + y += offset + } + if isRight { + x += offset + } + if isLeft { + x -= (b.Width() + offset) + } + seriesPainter.Text(name, x, y) + } + + // 雷达图 + angles := getPolygonPointAngles(sides) + maxCount := len(indicators) + for _, series := range seriesList { + linePoints := make([]Point, 0, maxCount) + for j, item := range series.Data { + if j >= maxCount { + continue + } + indicator := indicators[j] + percent := (item.Value - indicator.Min) / (indicator.Max - indicator.Min) + r := percent * radius + p := getPolygonPoint(center, r, angles[j]) + linePoints = append(linePoints, p) + } + color := theme.GetSeriesColor(series.index) + dotFillColor := drawing.ColorWhite + if theme.IsDark() { + dotFillColor = color + } + linePoints = append(linePoints, linePoints[0]) + seriesPainter.OverrideDrawingStyle(Style{ + StrokeColor: color, + StrokeWidth: defaultStrokeWidth, + DotWidth: defaultDotWidth, + DotColor: color, + FillColor: color.WithAlpha(20), + }) + seriesPainter.LineStroke(linePoints). + FillArea(linePoints) + dotWith := 2.0 + seriesPainter.OverrideDrawingStyle(Style{ + StrokeWidth: defaultStrokeWidth, + StrokeColor: color, + FillColor: dotFillColor, + }) + for _, point := range linePoints { + seriesPainter.Circle(dotWith, point.X, point.Y) + seriesPainter.FillStroke() + } + } + + return r.p.box, nil +} + +func (r *radarChart) Render() (Box, error) { + p := r.p + opt := r.opt + renderResult, err := defaultRender(p, defaultRenderOption{ + Theme: opt.Theme, + Padding: opt.Padding, + SeriesList: opt.SeriesList, + XAxis: XAxisOption{ + Show: FalseFlag(), + }, + YAxisOptions: []YAxisOption{ + { + Show: FalseFlag(), + }, + }, + TitleOption: opt.Title, + LegendOption: opt.Legend, + backgroundIsFilled: opt.backgroundIsFilled, + }) + if err != nil { + return BoxZero, err + } + seriesList := opt.SeriesList.Filter(ChartTypeRadar) + return r.render(renderResult, seriesList) +} diff --git a/xaxis.go b/xaxis.go index f06d71f..bfb57cb 100644 --- a/xaxis.go +++ b/xaxis.go @@ -38,8 +38,8 @@ type XAxisOption struct { Theme ColorPalette // The font size of x axis label FontSize float64 - // Hidden x axis - Hidden bool + // The flag for show axis, set this to *false will hide axis + Show *bool // Number of segments that the axis is split into. Note that this number serves only as a recommendation. SplitNumber int // The position of axis, it can be 'top' or 'bottom' @@ -78,6 +78,7 @@ func (opt *XAxisOption) ToAxisOption() AxisOption { FontSize: opt.FontSize, Font: opt.Font, FontColor: opt.FontColor, + Show: opt.Show, SplitLineColor: opt.Theme.GetAxisSplitLineColor(), } if opt.isValueAxis { diff --git a/yaxis.go b/yaxis.go index 609924f..265ac59 100644 --- a/yaxis.go +++ b/yaxis.go @@ -25,6 +25,10 @@ package charts import "github.com/golang/freetype/truetype" type YAxisOption struct { + // The minimun value of axis. + Min *float64 + // The maximum value of axis. + Max *float64 // The font of y axis Font *truetype.Font // The data value of x axis @@ -36,7 +40,13 @@ type YAxisOption struct { // The position of axis, it can be 'left' or 'right' Position string // The color of label - FontColor Color + FontColor Color + // Formatter for y axis text value + Formatter string + // Color for y axis + Color Color + // The flag for show axis, set this to *false will hide axis + Show *bool isCategoryAxis bool } @@ -60,6 +70,7 @@ func (opt *YAxisOption) ToAxisOption() AxisOption { position = PositionRight } axisOpt := AxisOption{ + Formatter: opt.Formatter, Theme: opt.Theme, Data: opt.Data, Position: position, @@ -70,6 +81,11 @@ func (opt *YAxisOption) ToAxisOption() AxisOption { BoundaryGap: FalseFlag(), SplitLineShow: true, SplitLineColor: opt.Theme.GetAxisSplitLineColor(), + Show: opt.Show, + } + if !opt.Color.IsZero() { + axisOpt.FontColor = opt.Color + axisOpt.StrokeColor = opt.Color } if opt.isCategoryAxis { axisOpt.BoundaryGap = TrueFlag() @@ -85,3 +101,13 @@ func NewLeftYAxis(p *Painter, opt YAxisOption) *axisPainter { })) return NewAxisPainter(p, opt.ToAxisOption()) } + +func NewRightYAxis(p *Painter, opt YAxisOption) *axisPainter { + p = p.Child(PainterPaddingOption(Box{ + Bottom: defaultXAxisHeight, + })) + axisOpt := opt.ToAxisOption() + axisOpt.Position = PositionRight + axisOpt.SplitLineShow = false + return NewAxisPainter(p, axisOpt) +} From 38c4978e44d8f92906d192041637d7ecad7c89d3 Mon Sep 17 00:00:00 2001 From: vicanso Date: Fri, 17 Jun 2022 23:37:21 +0800 Subject: [PATCH 019/116] refactor: enhance chart render function --- README.md | 32 ++-- bar_chart.go | 2 +- chart_option.go | 238 +++++++++++++++++++++++++- charts.go | 11 +- echarts.go | 18 ++ examples/bar_chart/main.go | 116 +++++-------- examples/charts/main.go | 59 +++++++ examples/funnel_chart/main.go | 62 ++----- examples/horizontal_bar_chart/main.go | 65 +++---- examples/line_chart/main.go | 135 +++++++-------- examples/pie_chart/main.go | 41 ++--- examples/radar_chart/main.go | 106 ++++-------- funnel_chart.go | 2 +- horizontal_bar_chart.go | 2 +- line_chart.go | 2 +- painter.go | 6 +- pie_chart.go | 2 +- radar_chart.go | 2 +- series.go | 8 + theme.go | 218 ++++++++++++----------- 20 files changed, 665 insertions(+), 462 deletions(-) diff --git a/README.md b/README.md index 22d3205..7affa30 100644 --- a/README.md +++ b/README.md @@ -49,25 +49,21 @@ func writeFile(file string, buf []byte) error { } func chartsRender() ([]byte, error) { - d, err := charts.LineRender([][]float64{ + values := [][]float64{ { - 150, + 120, + 132, + 101, + 134, + 90, 230, - 224, - 218, - 135, - 147, - 260, + 210, }, - }, - // output type - charts.PNGTypeOption(), - // title - charts.TitleOptionFunc(charts.TitleOption{ - Text: "Line", - }), - // x axis - charts.XAxisOptionFunc(charts.NewXAxisOption([]string{ + } + p, err := charts.LineRender( + values, + charts.TitleTextOptionFunc("Line"), + charts.XAxisDataOptionFunc([]string{ "Mon", "Tue", "Wed", @@ -75,12 +71,12 @@ func chartsRender() ([]byte, error) { "Fri", "Sat", "Sun", - })), + }), ) if err != nil { return nil, err } - return d.Bytes() + return p.Bytes() } func echartsRender() ([]byte, error) { diff --git a/bar_chart.go b/bar_chart.go index 8330542..2982829 100644 --- a/bar_chart.go +++ b/bar_chart.go @@ -34,7 +34,7 @@ type barChart struct { func NewBarChart(p *Painter, opt BarChartOption) *barChart { if opt.Theme == nil { - opt.Theme = NewTheme("") + opt.Theme = defaultTheme } return &barChart{ p: p, diff --git a/chart_option.go b/chart_option.go index 0cea754..0bc0a34 100644 --- a/chart_option.go +++ b/chart_option.go @@ -66,6 +66,182 @@ type ChartOption struct { Children []ChartOption } +// OptionFunc option function +type OptionFunc func(opt *ChartOption) + +// PNGTypeOption set png type of chart's output +func PNGTypeOption() OptionFunc { + return TypeOptionFunc(ChartOutputPNG) +} + +// TypeOptionFunc set type of chart's output +func TypeOptionFunc(t string) OptionFunc { + return func(opt *ChartOption) { + opt.Type = t + } +} + +// FontFamilyOptionFunc set font family of chart +func FontFamilyOptionFunc(fontFamily string) OptionFunc { + return func(opt *ChartOption) { + opt.FontFamily = fontFamily + } +} + +// ThemeOptionFunc set them of chart +func ThemeOptionFunc(theme string) OptionFunc { + return func(opt *ChartOption) { + opt.Theme = theme + } +} + +// TitleOptionFunc set title of chart +func TitleOptionFunc(title TitleOption) OptionFunc { + return func(opt *ChartOption) { + opt.Title = title + } +} + +// TitleTextOptionFunc set title text of chart +func TitleTextOptionFunc(text string) OptionFunc { + return func(opt *ChartOption) { + opt.Title.Text = text + } +} + +// LegendOptionFunc set legend of chart +func LegendOptionFunc(legend LegendOption) OptionFunc { + return func(opt *ChartOption) { + opt.Legend = legend + } +} + +// LegendLabelsOptionFunc set legend labels of chart +func LegendLabelsOptionFunc(labels []string, left ...string) OptionFunc { + return func(opt *ChartOption) { + opt.Legend = NewLegendOption(labels, left...) + } +} + +// XAxisOptionFunc set x axis of chart +func XAxisOptionFunc(xAxisOption XAxisOption) OptionFunc { + return func(opt *ChartOption) { + opt.XAxis = xAxisOption + } +} + +// XAxisDataOptionFunc set x axis data of chart +func XAxisDataOptionFunc(data []string, boundaryGap ...*bool) OptionFunc { + return func(opt *ChartOption) { + opt.XAxis = NewXAxisOption(data, boundaryGap...) + } +} + +// YAxisOptionFunc set y axis of chart, support two y axis +func YAxisOptionFunc(yAxisOption ...YAxisOption) OptionFunc { + return func(opt *ChartOption) { + opt.YAxisOptions = yAxisOption + } +} + +// YAxisDataOptionFunc set y axis data of chart +func YAxisDataOptionFunc(data []string) OptionFunc { + return func(opt *ChartOption) { + opt.YAxisOptions = NewYAxisOptions(data) + } +} + +// WidthOptionFunc set width of chart +func WidthOptionFunc(width int) OptionFunc { + return func(opt *ChartOption) { + opt.Width = width + } +} + +// HeightOptionFunc set height of chart +func HeightOptionFunc(height int) OptionFunc { + return func(opt *ChartOption) { + opt.Height = height + } +} + +// PaddingOptionFunc set padding of chart +func PaddingOptionFunc(padding Box) OptionFunc { + return func(opt *ChartOption) { + opt.Padding = padding + } +} + +// BoxOptionFunc set box of chart +func BoxOptionFunc(box Box) OptionFunc { + return func(opt *ChartOption) { + opt.Box = box + } +} + +// PieSeriesShowLabel set pie series show label +func PieSeriesShowLabel() OptionFunc { + return func(opt *ChartOption) { + for index := range opt.SeriesList { + opt.SeriesList[index].Label.Show = true + } + } +} + +// ChildOptionFunc add child chart +func ChildOptionFunc(child ...ChartOption) OptionFunc { + return func(opt *ChartOption) { + if opt.Children == nil { + opt.Children = make([]ChartOption, 0) + } + opt.Children = append(opt.Children, child...) + } +} + +// RadarIndicatorOptionFunc set radar indicator of chart +func RadarIndicatorOptionFunc(names []string, values []float64) OptionFunc { + return func(opt *ChartOption) { + if len(names) != len(values) { + return + } + indicators := make([]RadarIndicator, len(names)) + for index, name := range names { + indicators[index] = RadarIndicator{ + Name: name, + Max: values[index], + } + } + opt.RadarIndicators = indicators + } +} + +// BackgroundColorOptionFunc set background color of chart +func BackgroundColorOptionFunc(color Color) OptionFunc { + return func(opt *ChartOption) { + opt.BackgroundColor = color + } +} + +// MarkLineOptionFunc set mark line for series of chart +func MarkLineOptionFunc(seriesIndex int, markLineTypes ...string) OptionFunc { + return func(opt *ChartOption) { + if len(opt.SeriesList) <= seriesIndex { + return + } + opt.SeriesList[seriesIndex].MarkLine = NewMarkLine(markLineTypes...) + } +} + +// MarkPointOptionFunc set mark point for series of chart +func MarkPointOptionFunc(seriesIndex int, markPointTypes ...string) OptionFunc { + return func(opt *ChartOption) { + if len(opt.SeriesList) <= seriesIndex { + return + } + opt.SeriesList[seriesIndex].MarkPoint = NewMarkPoint(markPointTypes...) + } +} + func (o *ChartOption) fillDefault() { t := NewTheme(o.Theme) o.theme = t @@ -90,11 +266,11 @@ func (o *ChartOption) fillDefault() { o.BackgroundColor = t.GetBackgroundColor() } if o.Padding.IsZero() { - o.Padding = chart.Box{ - Top: 10, - Right: 10, - Bottom: 10, - Left: 10, + o.Padding = Box{ + Top: 20, + Right: 20, + Bottom: 20, + Left: 20, } } // legend与series name的关联 @@ -118,3 +294,55 @@ func (o *ChartOption) fillDefault() { }) } } + +// LineRender line chart render +func LineRender(values [][]float64, opts ...OptionFunc) (*Painter, error) { + seriesList := NewSeriesListDataFromValues(values, ChartTypeLine) + return Render(ChartOption{ + SeriesList: seriesList, + }, opts...) +} + +// BarRender bar chart render +func BarRender(values [][]float64, opts ...OptionFunc) (*Painter, error) { + seriesList := NewSeriesListDataFromValues(values, ChartTypeBar) + return Render(ChartOption{ + SeriesList: seriesList, + }, opts...) +} + +// HorizontalBarRender horizontal bar chart render +func HorizontalBarRender(values [][]float64, opts ...OptionFunc) (*Painter, error) { + seriesList := NewSeriesListDataFromValues(values, ChartTypeHorizontalBar) + return Render(ChartOption{ + SeriesList: seriesList, + }, opts...) +} + +// PieRender pie chart render +func PieRender(values []float64, opts ...OptionFunc) (*Painter, error) { + return Render(ChartOption{ + SeriesList: NewPieSeriesList(values), + }, opts...) +} + +// RadarRender radar chart render +func RadarRender(values [][]float64, opts ...OptionFunc) (*Painter, error) { + seriesList := NewSeriesListDataFromValues(values, ChartTypeRadar) + return Render(ChartOption{ + SeriesList: seriesList, + }, opts...) +} + +// FunnelRender funnel chart render +func FunnelRender(values []float64, opts ...OptionFunc) (*Painter, error) { + seriesList := make(SeriesList, len(values)) + for index, value := range values { + seriesList[index] = NewSeriesFromValues([]float64{ + value, + }, ChartTypeFunnel) + } + return Render(ChartOption{ + SeriesList: seriesList, + }, opts...) +} diff --git a/charts.go b/charts.go index 51e247a..5759367 100644 --- a/charts.go +++ b/charts.go @@ -239,7 +239,10 @@ func doRender(renderers ...Renderer) error { return nil } -func Render(opt ChartOption) (*Painter, error) { +func Render(opt ChartOption, opts ...OptionFunc) (*Painter, error) { + for _, fn := range opts { + fn(&opt) + } opt.fillDefault() isChild := true @@ -398,6 +401,12 @@ func Render(opt ChartOption) (*Painter, error) { } for _, item := range opt.Children { item.Parent = p + if item.Theme == "" { + item.Theme = opt.Theme + } + if item.FontFamily == "" { + item.FontFamily = opt.FontFamily + } _, err = Render(item) if err != nil { return nil, err diff --git a/echarts.go b/echarts.go index ac28436..d2602b3 100644 --- a/echarts.go +++ b/echarts.go @@ -130,6 +130,7 @@ type EChartsXAxisData struct { BoundaryGap *bool `json:"boundaryGap"` SplitNumber int `json:"splitNumber"` Data []string `json:"data"` + Type string `json:"type"` } type EChartsXAxis struct { Data []EChartsXAxisData @@ -155,6 +156,7 @@ type EChartsYAxisData struct { Color string `json:"color"` } `json:"lineStyle"` } `json:"axisLine"` + Data []string `json:"data"` } type EChartsYAxis struct { Data []EChartsYAxisData `json:"data"` @@ -453,6 +455,21 @@ func (eo *EChartsOption) ToOption() ChartOption { Box: eo.Box, SeriesList: eo.Series.ToSeriesList(), } + isHorizontalChart := false + for _, item := range eo.XAxis.Data { + if item.Type == "value" { + isHorizontalChart = true + } + } + if isHorizontalChart { + for index := range o.SeriesList { + series := o.SeriesList[index] + if series.Type == ChartTypeBar { + o.SeriesList[index].Type = ChartTypeHorizontalBar + } + } + } + if len(eo.XAxis.Data) != 0 { xAxisData := eo.XAxis.Data[0] o.XAxis = XAxisOption{ @@ -468,6 +485,7 @@ func (eo *EChartsOption) ToOption() ChartOption { Max: item.Max, Formatter: item.AxisLabel.Formatter, Color: parseColor(item.AxisLine.LineStyle.Color), + Data: item.Data, } } o.YAxisOptions = yAxisOptions diff --git a/examples/bar_chart/main.go b/examples/bar_chart/main.go index 5d5da2a..c9f1d58 100644 --- a/examples/bar_chart/main.go +++ b/examples/bar_chart/main.go @@ -24,26 +24,39 @@ func writeFile(buf []byte) error { } func main() { - p, err := charts.NewPainter(charts.PainterOptions{ - Width: 800, - Height: 600, - Type: charts.ChartOutputPNG, - }) - if err != nil { - panic(err) + values := [][]float64{ + { + 2.0, + 4.9, + 7.0, + 23.2, + 25.6, + 76.7, + 135.6, + 162.2, + 32.6, + 20.0, + 6.4, + 3.3, + }, + { + 2.6, + 5.9, + 9.0, + 26.4, + 28.7, + 70.7, + 175.6, + 182.2, + 48.7, + 18.8, + 6.0, + 2.3, + }, } - _, err = charts.NewBarChart(p, charts.BarChartOption{ - Title: charts.TitleOption{ - Text: "Rainfall vs Evaporation", - Subtext: "Fake Data", - }, - Padding: charts.Box{ - Top: 20, - Right: 20, - Bottom: 20, - Left: 20, - }, - XAxis: charts.NewXAxisOption([]string{ + p, err := charts.BarRender( + values, + charts.XAxisDataOptionFunc([]string{ "Jan", "Feb", "Mar", @@ -57,61 +70,24 @@ func main() { "Nov", "Dec", }), - Legend: charts.NewLegendOption([]string{ + charts.LegendLabelsOptionFunc([]string{ "Rainfall", "Evaporation", }, charts.PositionRight), - SeriesList: []charts.Series{ - { - Type: charts.ChartTypeBar, - Data: charts.NewSeriesDataFromValues([]float64{ - 2.0, - 4.9, - 7.0, - 23.2, - 25.6, - 76.7, - 135.6, - 162.2, - 32.6, - 20.0, - 6.4, - 3.3, - }), - MarkPoint: charts.NewMarkPoint( - charts.SeriesMarkDataTypeMax, - charts.SeriesMarkDataTypeMin, - ), - MarkLine: charts.NewMarkLine( - charts.SeriesMarkDataTypeAverage, - ), - }, - { - Type: charts.ChartTypeBar, - Data: charts.NewSeriesDataFromValues([]float64{ - 2.6, - 5.9, - 9.0, - 26.4, - 28.7, - 70.7, - 175.6, - 182.2, - 48.7, - 18.8, - 6.0, - 2.3, - }), - MarkPoint: charts.NewMarkPoint( - charts.SeriesMarkDataTypeMax, - charts.SeriesMarkDataTypeMin, - ), - MarkLine: charts.NewMarkLine( - charts.SeriesMarkDataTypeAverage, - ), - }, + charts.MarkLineOptionFunc(0, charts.SeriesMarkDataTypeAverage), + charts.MarkPointOptionFunc(0, charts.SeriesMarkDataTypeMax, + charts.SeriesMarkDataTypeMin), + // custom option func + func(opt *charts.ChartOption) { + opt.SeriesList[1].MarkPoint = charts.NewMarkPoint( + charts.SeriesMarkDataTypeMax, + charts.SeriesMarkDataTypeMin, + ) + opt.SeriesList[1].MarkLine = charts.NewMarkLine( + charts.SeriesMarkDataTypeAverage, + ) }, - }).Render() + ) if err != nil { panic(err) } diff --git a/examples/charts/main.go b/examples/charts/main.go index 3a625f7..b370b69 100644 --- a/examples/charts/main.go +++ b/examples/charts/main.go @@ -73,6 +73,7 @@ func handler(w http.ResponseWriter, req *http.Request, chartOptions []charts.Cha bytesList := make([][]byte, 0) for _, opt := range chartOptions { opt.Theme = theme + opt.Type = charts.ChartOutputSVG d, err := charts.Render(opt) if err != nil { panic(err) @@ -1055,6 +1056,64 @@ func echartsHandler(w http.ResponseWriter, req *http.Request) { } ] }`, + `{ + "title": { + "text": "World Population" + }, + "tooltip": { + "trigger": "axis", + "axisPointer": { + "type": "shadow" + } + }, + "legend": {}, + "grid": { + "left": "3%", + "right": "4%", + "bottom": "3%", + "containLabel": true + }, + "xAxis": { + "type": "value" + }, + "yAxis": { + "type": "category", + "data": [ + "Brazil", + "Indonesia", + "USA", + "India", + "China", + "World" + ] + }, + "series": [ + { + "name": "2011", + "type": "bar", + "data": [ + 18203, + 23489, + 29034, + 104970, + 131744, + 630230 + ] + }, + { + "name": "2012", + "type": "bar", + "data": [ + 19325, + 23438, + 31000, + 121594, + 134141, + 681807 + ] + } + ] + }`, `{ "title": { "text": "Rainfall vs Evaporation", diff --git a/examples/funnel_chart/main.go b/examples/funnel_chart/main.go index eb753fd..6b17614 100644 --- a/examples/funnel_chart/main.go +++ b/examples/funnel_chart/main.go @@ -24,64 +24,24 @@ func writeFile(buf []byte) error { } func main() { - p, err := charts.NewPainter(charts.PainterOptions{ - Width: 800, - Height: 600, - Type: charts.ChartOutputPNG, - }) - if err != nil { - panic(err) + values := []float64{ + 100, + 80, + 60, + 40, + 20, } - _, err = charts.NewFunnelChart(p, charts.FunnelChartOption{ - Title: charts.TitleOption{ - Text: "Funnel", - }, - Legend: charts.NewLegendOption([]string{ + p, err := charts.FunnelRender( + values, + charts.TitleTextOptionFunc("Funnel"), + charts.LegendLabelsOptionFunc([]string{ "Show", "Click", "Visit", "Inquiry", "Order", }), - SeriesList: []charts.Series{ - - { - Type: charts.ChartTypeFunnel, - Name: "Show", - Data: charts.NewSeriesDataFromValues([]float64{ - 100, - }), - }, - { - Type: charts.ChartTypeFunnel, - Name: "Click", - Data: charts.NewSeriesDataFromValues([]float64{ - 80, - }), - }, - { - Type: charts.ChartTypeFunnel, - Name: "Visit", - Data: charts.NewSeriesDataFromValues([]float64{ - 60, - }), - }, - { - Type: charts.ChartTypeFunnel, - Name: "Inquiry", - Data: charts.NewSeriesDataFromValues([]float64{ - 40, - }), - }, - { - Type: charts.ChartTypeFunnel, - Name: "Order", - Data: charts.NewSeriesDataFromValues([]float64{ - 20, - }), - }, - }, - }).Render() + ) if err != nil { panic(err) } diff --git a/examples/horizontal_bar_chart/main.go b/examples/horizontal_bar_chart/main.go index eecd9ec..6b206b0 100644 --- a/examples/horizontal_bar_chart/main.go +++ b/examples/horizontal_bar_chart/main.go @@ -24,29 +24,38 @@ func writeFile(buf []byte) error { } func main() { - p, err := charts.NewPainter(charts.PainterOptions{ - Width: 800, - Height: 600, - Type: charts.ChartOutputPNG, - }) - if err != nil { - panic(err) - } - _, err = charts.NewHorizontalBarChart(p, charts.HorizontalBarChartOption{ - Title: charts.TitleOption{ - Text: "World Population", + values := [][]float64{ + { + 18203, + 23489, + 29034, + 104970, + 131744, + 630230, }, - Padding: charts.Box{ + { + 19325, + 23438, + 31000, + 121594, + 134141, + 681807, + }, + } + p, err := charts.HorizontalBarRender( + values, + charts.TitleTextOptionFunc("World Population"), + charts.PaddingOptionFunc(charts.Box{ Top: 20, Right: 40, Bottom: 20, Left: 20, - }, - Legend: charts.NewLegendOption([]string{ + }), + charts.LegendLabelsOptionFunc([]string{ "2011", "2012", }), - YAxisOptions: charts.NewYAxisOptions([]string{ + charts.YAxisDataOptionFunc([]string{ "Brazil", "Indonesia", "USA", @@ -54,31 +63,7 @@ func main() { "China", "World", }), - SeriesList: []charts.Series{ - { - Type: charts.ChartTypeHorizontalBar, - Data: charts.NewSeriesDataFromValues([]float64{ - 18203, - 23489, - 29034, - 104970, - 131744, - 630230, - }), - }, - { - Type: charts.ChartTypeHorizontalBar, - Data: charts.NewSeriesDataFromValues([]float64{ - 19325, - 23438, - 31000, - 121594, - 134141, - 681807, - }), - }, - }, - }).Render() + ) if err != nil { panic(err) } diff --git a/examples/line_chart/main.go b/examples/line_chart/main.go index 414f676..435da78 100644 --- a/examples/line_chart/main.go +++ b/examples/line_chart/main.go @@ -24,35 +24,57 @@ func writeFile(buf []byte) error { } func main() { - p, err := charts.NewPainter(charts.PainterOptions{ - Width: 800, - Height: 600, - Type: charts.ChartOutputPNG, - }) - if err != nil { - panic(err) + values := [][]float64{ + { + 120, + 132, + 101, + 134, + 90, + 230, + 210, + }, + { + 220, + 182, + 191, + 234, + 290, + 330, + 310, + }, + { + 150, + 232, + 201, + 154, + 190, + 330, + 410, + }, + { + 320, + 332, + 301, + 334, + 390, + 330, + 320, + }, + { + 820, + 932, + 901, + 934, + 1290, + 1330, + 1320, + }, } - _, err = charts.NewLineChart(p, charts.LineChartOption{ - Padding: charts.Box{ - Left: 10, - Top: 10, - Right: 10, - Bottom: 10, - }, - Title: charts.TitleOption{ - Text: "Line", - }, - Legend: charts.LegendOption{ - Data: []string{ - "Email", - "Union Ads", - "Video Ads", - "Direct", - "Search Engine", - }, - Left: charts.PositionCenter, - }, - XAxis: charts.NewXAxisOption([]string{ + p, err := charts.LineRender( + values, + charts.TitleTextOptionFunc("Line"), + charts.XAxisDataOptionFunc([]string{ "Mon", "Tue", "Wed", @@ -61,54 +83,15 @@ func main() { "Sat", "Sun", }), - SeriesList: charts.SeriesList{ - charts.NewSeriesFromValues([]float64{ - 120, - 132, - 101, - 134, - 90, - 230, - 210, - }), - charts.NewSeriesFromValues([]float64{ - 220, - 182, - 191, - 234, - 290, - 330, - 310, - }), - charts.NewSeriesFromValues([]float64{ - 150, - 232, - 201, - 154, - 190, - 330, - 410, - }), - charts.NewSeriesFromValues([]float64{ - 320, - 332, - 301, - 334, - 390, - 330, - 320, - }), - charts.NewSeriesFromValues([]float64{ - 820, - 932, - 901, - 934, - 1290, - 1330, - 1320, - }), - }, - }).Render() + charts.LegendLabelsOptionFunc([]string{ + "Email", + "Union Ads", + "Video Ads", + "Direct", + "Search Engine", + }, charts.PositionCenter), + ) + if err != nil { panic(err) } diff --git a/examples/pie_chart/main.go b/examples/pie_chart/main.go index e69bf60..8a98e57 100644 --- a/examples/pie_chart/main.go +++ b/examples/pie_chart/main.go @@ -24,27 +24,27 @@ func writeFile(buf []byte) error { } func main() { - p, err := charts.NewPainter(charts.PainterOptions{ - Width: 800, - Height: 600, - Type: charts.ChartOutputPNG, - }) - if err != nil { - panic(err) + values := []float64{ + 1048, + 735, + 580, + 484, + 300, } - _, err = charts.NewPieChart(p, charts.PieChartOption{ - Title: charts.TitleOption{ + p, err := charts.PieRender( + values, + charts.TitleOptionFunc(charts.TitleOption{ Text: "Rainfall vs Evaporation", Subtext: "Fake Data", Left: charts.PositionCenter, - }, - Padding: charts.Box{ + }), + charts.PaddingOptionFunc(charts.Box{ Top: 20, Right: 20, Bottom: 20, Left: 20, - }, - Legend: charts.LegendOption{ + }), + charts.LegendOptionFunc(charts.LegendOption{ Orient: charts.OrientVertical, Data: []string{ "Search Engine", @@ -54,20 +54,9 @@ func main() { "Video Ads", }, Left: charts.PositionLeft, - }, - SeriesList: charts.NewPieSeriesList([]float64{ - 1048, - 735, - 580, - 484, - 300, - }, charts.PieSeriesOption{ - Label: charts.SeriesLabel{ - Show: true, - }, - Radius: "35%", }), - }).Render() + charts.PieSeriesShowLabel(), + ) if err != nil { panic(err) } diff --git a/examples/radar_chart/main.go b/examples/radar_chart/main.go index 077fa48..9550951 100644 --- a/examples/radar_chart/main.go +++ b/examples/radar_chart/main.go @@ -24,79 +24,47 @@ func writeFile(buf []byte) error { } func main() { - p, err := charts.NewPainter(charts.PainterOptions{ - Width: 800, - Height: 600, - Type: charts.ChartOutputPNG, - }) - if err != nil { - panic(err) + values := [][]float64{ + { + 4200, + 3000, + 20000, + 35000, + 50000, + 18000, + }, + { + 5000, + 14000, + 28000, + 26000, + 42000, + 21000, + }, } - _, err = charts.NewRadarChart(p, charts.RadarChartOption{ - Padding: charts.Box{ - Left: 10, - Top: 10, - Right: 10, - Bottom: 10, - }, - Title: charts.TitleOption{ - Text: "Basic Radar Chart", - }, - Legend: charts.NewLegendOption([]string{ + p, err := charts.RadarRender( + values, + charts.TitleTextOptionFunc("Basic Radar Chart"), + charts.LegendLabelsOptionFunc([]string{ "Allocated Budget", "Actual Spending", }), - RadarIndicators: []charts.RadarIndicator{ - { - Name: "Sales", - Max: 6500, - }, - { - Name: "Administration", - Max: 16000, - }, - { - Name: "Information Technology", - Max: 30000, - }, - { - Name: "Customer Support", - Max: 38000, - }, - { - Name: "Development", - Max: 52000, - }, - { - Name: "Marketing", - Max: 25000, - }, - }, - SeriesList: charts.SeriesList{ - { - Type: charts.ChartTypeRadar, - Data: charts.NewSeriesDataFromValues([]float64{ - 4200, - 3000, - 20000, - 35000, - 50000, - 18000, - }), - }, - { - Type: charts.ChartTypeRadar, - Data: charts.NewSeriesDataFromValues([]float64{ - 5000, - 14000, - 28000, - 26000, - 42000, - 21000, - }), - }, - }, - }).Render() + charts.RadarIndicatorOptionFunc([]string{ + "Sales", + "Administration", + "Information Technology", + "Customer Support", + "Development", + "Marketing", + }, []float64{ + 6500, + 16000, + 30000, + 38000, + 52000, + 25000, + }), + ) if err != nil { panic(err) } diff --git a/funnel_chart.go b/funnel_chart.go index c8457dd..63b3504 100644 --- a/funnel_chart.go +++ b/funnel_chart.go @@ -36,7 +36,7 @@ type funnelChart struct { func NewFunnelChart(p *Painter, opt FunnelChartOption) *funnelChart { if opt.Theme == nil { - opt.Theme = NewTheme("") + opt.Theme = defaultTheme } return &funnelChart{ p: p, diff --git a/horizontal_bar_chart.go b/horizontal_bar_chart.go index c98d688..fb23734 100644 --- a/horizontal_bar_chart.go +++ b/horizontal_bar_chart.go @@ -52,7 +52,7 @@ type HorizontalBarChartOption struct { func NewHorizontalBarChart(p *Painter, opt HorizontalBarChartOption) *horizontalBarChart { if opt.Theme == nil { - opt.Theme = NewTheme("") + opt.Theme = defaultTheme } return &horizontalBarChart{ p: p, diff --git a/line_chart.go b/line_chart.go index c505a91..0dc0fd8 100644 --- a/line_chart.go +++ b/line_chart.go @@ -34,7 +34,7 @@ type lineChart struct { func NewLineChart(p *Painter, opt LineChartOption) *lineChart { if opt.Theme == nil { - opt.Theme = NewTheme("") + opt.Theme = defaultTheme } return &lineChart{ p: p, diff --git a/painter.go b/painter.go index 5a8dd89..0bacd3c 100644 --- a/painter.go +++ b/painter.go @@ -149,9 +149,9 @@ func NewPainter(opts PainterOptions, opt ...PainterOption) (*Painter, error) { } font = f } - fn := chart.SVG - if opts.Type == ChartOutputPNG { - fn = chart.PNG + fn := chart.PNG + if opts.Type == ChartOutputSVG { + fn = chart.SVG } width := opts.Width height := opts.Height diff --git a/pie_chart.go b/pie_chart.go index c5a2ff2..972b4c1 100644 --- a/pie_chart.go +++ b/pie_chart.go @@ -53,7 +53,7 @@ type PieChartOption struct { func NewPieChart(p *Painter, opt PieChartOption) *pieChart { if opt.Theme == nil { - opt.Theme = NewTheme("") + opt.Theme = defaultTheme } return &pieChart{ p: p, diff --git a/radar_chart.go b/radar_chart.go index dc93ca8..610d5f7 100644 --- a/radar_chart.go +++ b/radar_chart.go @@ -64,7 +64,7 @@ type RadarChartOption struct { func NewRadarChart(p *Painter, opt RadarChartOption) *radarChart { if opt.Theme == nil { - opt.Theme = NewTheme("") + opt.Theme = defaultTheme } return &radarChart{ p: p, diff --git a/series.go b/series.go index 4808bcb..905c140 100644 --- a/series.go +++ b/series.go @@ -36,6 +36,14 @@ type SeriesData struct { Style Style } +func NewSeriesListDataFromValues(values [][]float64, chartType ...string) SeriesList { + seriesList := make(SeriesList, len(values)) + for index, value := range values { + seriesList[index] = NewSeriesFromValues(value, chartType...) + } + return seriesList +} + func NewSeriesFromValues(values []float64, chartType ...string) Series { s := Series{ Data: NewSeriesDataFromValues(values), diff --git a/theme.go b/theme.go index 544588a..26786b9 100644 --- a/theme.go +++ b/theme.go @@ -55,10 +55,21 @@ type themeColorPalette struct { font *truetype.Font } +type ThemeOption struct { + IsDarkMode bool + AxisStrokeColor Color + AxisSplitLineColor Color + BackgroundColor Color + TextColor Color + SeriesColors []Color +} + var palettes = map[string]ColorPalette{} const defaultFontSize = 12.0 +var defaultTheme ColorPalette + func init() { echartSeriesColors := []Color{ parseColor("#5470c6"), @@ -93,121 +104,134 @@ func init() { } AddTheme( ThemeDark, - true, - Color{ - R: 185, - G: 184, - B: 206, - A: 255, + ThemeOption{ + IsDarkMode: true, + AxisStrokeColor: Color{ + R: 185, + G: 184, + B: 206, + A: 255, + }, + AxisSplitLineColor: Color{ + R: 72, + G: 71, + B: 83, + A: 255, + }, + BackgroundColor: Color{ + R: 16, + G: 12, + B: 42, + A: 255, + }, + TextColor: Color{ + R: 238, + G: 238, + B: 238, + A: 255, + }, + SeriesColors: echartSeriesColors, }, - Color{ - R: 72, - G: 71, - B: 83, - A: 255, - }, - Color{ - R: 16, - G: 12, - B: 42, - A: 255, - }, - Color{ - R: 238, - G: 238, - B: 238, - A: 255, - }, - echartSeriesColors, ) AddTheme( ThemeLight, - false, - Color{ - R: 110, - G: 112, - B: 121, - A: 255, + ThemeOption{ + IsDarkMode: false, + AxisStrokeColor: Color{ + R: 110, + G: 112, + B: 121, + A: 255, + }, + AxisSplitLineColor: Color{ + R: 224, + G: 230, + B: 242, + A: 255, + }, + BackgroundColor: drawing.ColorWhite, + TextColor: Color{ + R: 70, + G: 70, + B: 70, + A: 255, + }, + SeriesColors: echartSeriesColors, }, - Color{ - R: 224, - G: 230, - B: 242, - A: 255, - }, - drawing.ColorWhite, - drawing.Color{ - R: 70, - G: 70, - B: 70, - A: 255, - }, - echartSeriesColors, ) AddTheme( ThemeAnt, - false, - Color{ - R: 110, - G: 112, - B: 121, - A: 255, + ThemeOption{ + IsDarkMode: false, + AxisStrokeColor: Color{ + R: 110, + G: 112, + B: 121, + A: 255, + }, + AxisSplitLineColor: Color{ + R: 224, + G: 230, + B: 242, + A: 255, + }, + BackgroundColor: drawing.ColorWhite, + TextColor: drawing.Color{ + R: 70, + G: 70, + B: 70, + A: 255, + }, + SeriesColors: antSeriesColors, }, - Color{ - R: 224, - G: 230, - B: 242, - A: 255, - }, - drawing.ColorWhite, - drawing.Color{ - R: 70, - G: 70, - B: 70, - A: 255, - }, - antSeriesColors, ) AddTheme( ThemeGrafana, - true, - drawing.Color{ - R: 185, - G: 184, - B: 206, - A: 255, + ThemeOption{ + IsDarkMode: true, + AxisStrokeColor: Color{ + R: 185, + G: 184, + B: 206, + A: 255, + }, + AxisSplitLineColor: Color{ + R: 68, + G: 67, + B: 67, + A: 255, + }, + BackgroundColor: drawing.Color{ + R: 31, + G: 29, + B: 29, + A: 255, + }, + TextColor: Color{ + R: 216, + G: 217, + B: 218, + A: 255, + }, + SeriesColors: grafanaSeriesColors, }, - drawing.Color{ - R: 68, - G: 67, - B: 67, - A: 255, - }, - drawing.Color{ - R: 31, - G: 29, - B: 29, - A: 255, - }, - drawing.Color{ - R: 216, - G: 217, - B: 218, - A: 255, - }, - grafanaSeriesColors, ) + SetDefaultTheme(ThemeLight) } -func AddTheme(name string, isDarkMode bool, axisStrokeColor, axisSplitLineColor, backgroundColor, textColor drawing.Color, seriesColors []drawing.Color) { +func SetDefaultTheme(name string) { + defaultTheme = NewTheme(name) +} + +func AddTheme(name string, opt ThemeOption) { palettes[name] = &themeColorPalette{ - isDarkMode: isDarkMode, - axisStrokeColor: axisStrokeColor, - axisSplitLineColor: axisSplitLineColor, - backgroundColor: backgroundColor, - textColor: textColor, - seriesColors: seriesColors, + isDarkMode: opt.IsDarkMode, + axisStrokeColor: opt.AxisStrokeColor, + axisSplitLineColor: opt.AxisSplitLineColor, + backgroundColor: opt.BackgroundColor, + textColor: opt.TextColor, + seriesColors: opt.SeriesColors, } } From 5db24de7ed177121f13cd2a8d62b18426e420a46 Mon Sep 17 00:00:00 2001 From: vicanso Date: Sat, 18 Jun 2022 08:55:46 +0800 Subject: [PATCH 020/116] refactor: add example for chinese --- README_zh.md | 4 +- assets/go-charts.png | Bin 372652 -> 340210 bytes axis.go | 3 + charts.go | 1 + examples/chinese/main.go | 118 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 examples/chinese/main.go diff --git a/README_zh.md b/README_zh.md index 57d9db4..2d16b04 100644 --- a/README_zh.md +++ b/README_zh.md @@ -220,4 +220,6 @@ BenchmarkMultiChartSVGRender-8 367 3356325 ns/op 默认使用的字符为`roboto`为英文字体库,因此如果需要显示中文字符需要增加中文字体库,`InstallFont`函数可添加对应的字体库,成功添加之后则指定`title.textStyle.fontFamily`即可。 在浏览器中使用`svg`时,如果指定的`fontFamily`不支持中文字符,展示的中文并不会乱码,但是会导致在计算字符宽度等错误。 -[中文字库noto-cjk](https://github.com/googlefonts/noto-cjk) \ No newline at end of file +字体文件可以在[中文字库noto-cjk](https://github.com/googlefonts/noto-cjk)下载,注意下载时选择字体格式为 `ttf` 格式,如果选用 `otf` 格式可能会加载失败。 + +示例见 [examples/chinese/main.go](examples/chinese/main.go) \ No newline at end of file diff --git a/assets/go-charts.png b/assets/go-charts.png index 5ead96171cd7989ba1666b8492f341425a701a95..a80e24112b8cb0717c1866b20152a56f3a50cf21 100644 GIT binary patch literal 340210 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%0D6~1LH>ENm6 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%0ARZYXFO_3h|Utj(s1Qb9QiHJ92ZS!~CvAoR66V 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Ȧ@}U3x+-;Jv|;DY=kz{9BKN5wNwjfH zB90)%$&Ya0CY(Vfyj=B9jIaa8xa^h=O+i7pX-bsKJ$n-4-XcrQy#JK=dEy}^q697PzEf9hz`blSeC zSSAy_VkZbm%|LCli^B?NIjGDx*>v-c931Qas;Iq4g)qBkwjJ7wd|nglm(0zpTO6Bw zvF;sy&^HYEpga@ORQBINf`xczF$+eKR|K?(p9;HN>X`p-f6_TEqocUH4^4tU`{wsc z=6JQ(UI%AMfa34;aQF<2Yi(rM_3~rC6Z-I0BEp^BvrI@B^k`%pWVI&mD#iu6;QbFu zTwGs2aBy*@k`Fb_lxO077D=pE?qgjFl0PmZ;d14^5nhEQk-JuKRBM~*HVn^9DKyrh zxuHIOp8ir1Rz~*;U@MYK#v*i)!_S9LkV>B!nT4l3xyf&%md;Zr7~f$z!j-?&u@B$u z3joM%ubV^~1uUV7L7GWQ`+J~=Q7(Lk=J$YWvie~w_A`1B7E9j5d7x3qE60LJJ_MT; z%tCnV{o0id z4}EcaQW4l#utdKrVpOwYkY|1Af#1{R@>8?zj2yGe!x6lZ{f!#53!$wy#4x#QgKP&5%J_j1NMFojlR;-Gms zf9fiBX`B{?M%7RxV>=BmL~k*bi?&k=a%N{w-}=M~=<;M5uWDE9jZ(k0ol4NBnkFim z^Xw)1{)lJNH$)|#1&7N>{)mpxOs2U|$ z(&^MzoK3aQl@3ikDfyj6MQ)ZE2^pR)K?DV5tCJbw%(~ZKju)f{HsuIMa`SckUNzge zB)~5nG&S}7@0BlZmQ^t0PB3n!>wD+svnL}Z44|zYG3N3BFp?H?Sy4xD`*xjA%Wu%2 zztX>ASo{4j{c2dRHCQ_%kVBm%*`Aud?-+8_6`h{wHpg+apP5fEg4=DY7FzMDM>32y z&6SXFJA8B~r1~c-^THsLS>IIaubUOccOWh!3RP_%wbZKmvZl~5&Rb0SlrTXp&3ochaIRb9(vPEe?rUzz0D7>B{dX8e@Xvm4NhQP+Y~rw#^GWbWGi zKc`hzmlY6E!#M2~E1DS;Br0L^?_RRVxep$u8u&x#zW~%5qY_8DV?=Is;!Gkgdsk^&OxxlSKAu%~h{B`CAB!@VI!6)9bgDnc z2o-;}D=~&N2Gt@CMwjVL`X$@+M~{x0oXD_8?tds};YckYqwf40@sXu4ek(k%j4?&~ z*R0i|91$bmM;VqFpPU_i z8Z@~TBg)1Nj1FVc*O|V+H>D#cal$>*J&k&$q12PKr$T-ZezVTI@P4<Bz8kAG@TH{LcXy4GVh?aWm%Z>(#J&lVpd3kw2*t=1Q&kqFgP=PfrU zTM;BoQ2mP+XSDhR@reRjDE3+B1lz=Fj5Gbr;z}akS=6zKaMPe(63XxDN&en68*F|F z39S%iW~`1nHdlCi>#qT`)5QxXUbXV~pAneDkga1nw)DQRuke)R*qP!5{ovF`K4sjz zo#(0(C3RYKF0;)|5qh%3EHo%$BXmK|^t_QP`ywvH%?Ekqvt-`yoz){{;4hGSQukI1 z7i529bB9gh&i*U70A7Y*Nat@*IIQaiM=@vN%V3((T9 zo4(HOoW@r7WsSC~5Pi=&R@bHFZLmU_S#52Bv&O0vy4G6(ZqMd5EsHtM=@))ossDy>GS=*o8NO~uw+6hiRb^67c7!cGj&FI0g} zEK;~hW8@wKK+`?=V$+iF&XVFq75x9h*Iz)@(KKtrFu@_Xy9EMlEI0&rcL)x_2^u80 z2Y1`JI|L2x!GpWILvVNahTQk_oO9NC|9`E$nCY&nuI`?m?xv>tDiLRaNcBSH0yFIn zi>Vrp;DbdYw{QbInXEzh^lUhWbT?Htb@FBwe4fzfkTMv$v6;#6y5sj-I>U7YxhzKa ztXxUa0jc8Kr`elH{73t?saVBW0Oa-Q^?4H&FJJ54=+n0prr2D_-<4Cu$lWnC-n?hQ zsJ%@oB!#t~z9$MA9t0$@$BieE1pnD08_L*!TbokO)b#N%fb#VCc5a?lV^f z8sNQss$ZStA@j2B?oVNxs!>M^h$t-WEqc5z{vyeJ&Tc~hRzS`4=@r;>Z>G(#V4ng_ zXT{99eO|z{Ah^>6V+lIrtN6PnKa`?wF@hSJ#y^IMZD)7VG{PkeT?cTGqkSv(z5UvV zN{NBZ%PR{!orIUXsRfLFuQ6MWblblElU$kvvj z(bg;67LqHIj9wRR4*ET^=q`hZ@E10aqt;u2zLlUY8Rq!K{-RLa*|m=v^a*MqO+}&Q zo?^jPVbgfn0aGE7Uc+AN*e&Qk38XU9{mOS5 zN@-=89PGQ}B?>9l1#vcRNP$DBi1x*f<|12u&F0EUkmK1Pi>8y?M}xGLZ6>5}o+Vvy z^b!;6!&$FbI`(8~q7q4OeRawp7I zwSHx=|5H8Wb+4A24QsbB-043ol)aAvI>`avZ@METrQdncALlxx1PxBs8MyMwqZ_?R zq8>F6Gn6-<3aBh3kK?*lc&P)FGCOk#aIyL`bxH9xUtu9nAwlAkp-?3(AkbRNIr_ztwX;q**iINySe+3c9rMm1|D ziNrs&8^WWd6O*+t{QK4}i0VRh-6t5Zq7?qq%Luj+i3%< zF5E{orZuHN%&W+m#2-B!;SD# zbNvO~{aao<368tZ+cm}|(Pn6&~7 zfzPB=GedTLz|KQw5We$Hfh~#Ck|H^ohnrT`zMW`s<&z3iC+;G((T8p$OEn*0$0acNt|+w#Ter?NXB|@OsyNP|y~c<{b`uRk6czT|cfE z)G!e-<^&4jQ0538mEsZ)N4x_QmQ1b za%eY^xEVOf=>4rMT+10o8mg+?1M-oluE%V{Kg`<&^ir5RE+0R=@B_nJyeCaeGp6mC zpQt@el82adr{Y)4&t>xuI=x4VGR{!gD&pG#Ysxa{a_^0*UO#Nl_{xF`y<%5kJ{rwh zFtK2$PeR5#Ws?@bHVfL1Dl8gQRe=pcO^Mp1#)2~m{c9bl4RgXEm1*_e0}X(h#$S3K zajC`m>h>HIpYiWc$ODa-%&Y8h%<<55M?xO~6yClBOZ0xJ$;0WtJui zJr$o{hj1i974O9nXu0#Fvm)AdStFlk_?B~95+AGy?1oCowvN*M^%rQ*4#r=fR?9Y+{6DF7Of1M z6J}#aKp&!+WM-9X#$(Ycip$BP683s>ATw8D>7~zcC3Ugew9r8!ZQm6@0>3=>KHO|B zgDgg?2WHF4FWlJgW!wG`9*n8YN?|ugnj5=G0-*BQnL*3eGc;0K?a~rA<>P&Dv^!-w zC&$$LU!6*EV7PZ$d09FK9vuCk&KM#d-zkoW<52?8-!U|rBo~4Gp{;!Uh=-Pm8{1K<7z1ND_Ga^V%5f}>jC})7l3LxM#Y2a!( zs7}~P%R#B7q@=6v{>GFW^->!AoZg6;-%=ROqXZ~37YbTem-q+`a01ZdJD36smn`-wrc7p;b?vu*oH!JM z+$uCSw`<&Zk01dI{->wZ&QG9s%LMdPJH;XFS`#gBr^5$ES z(ypAr<|=Q=iy~tMZdcUwkt-wMG8;__8^+!D)7?FuyLsaa@X`ks_1(%^doF{r|m5eB4BHG zC=PtfRY*jXMEwuAf9Jo^0erHB{|O)P8b_cTh<)%u^%bQb2-fibE1rN)HpZ!b-T!3< zaQ`1fdcM@VzrAJhe1+wQY+M%k^c@l??J?b2v*`MSC-0CC4y&yb=T!gkRUxUIg7@?p zCQM052{sUGU}H5+diAFq`?P|ZW0h)lhYZwnSZ1(SrSo!!rrIK|=N}a%B}L)5a<(s0 zG!*c`jzU=AdzUF6K}jlduqr$n2F8bgOMm}MXvv~%L`nD_M87OQ#9%UbULo`u+g0{P z*5uovN7Kj6?Imt^(^2Qa#N_6-){}lcvlGwAPjafynX%yXsH8bcENrk~7!HG0jd{+w zbFF6Fdvn7{v{6oL3*KC`cvVU&9Ey*iCk4@blZzemxwS8B9@qF~TD2C__nfM|%8|v= znBcHPukPYWv#&?@OmjcWkW!`V3}?$dX+X~>qWPs(sMm+{wwlZJCs`gOl^=Scy3I`p zkC)wlDIOIrC|Fk8(+6CIght1`-2`oteH zLumVy#FRj2qPannzXbyf^U z{p$NWM;iENZy>*`uk}U{7J=YU;@|N!qh)$JL1ud02W(%4G097F(BcQ75U)4Ffajp! zRcWD-da_a%{{FXqnfCNx=IL|M?HITNGDce zHWRd7NTk9buUaHW$SJ%TI67d*dg*=j4T z_ZmVc-7}x4ob*3hgd6@5(^|Yyx_i1=E&i0insNJgvwE2?Josv zDv;^-R%5LaUE9FKk8Pr0BNEzT$W zvUi(N0$(o@YNcguR@+3cbHbXRs)}^^!hX{zYEQMG-cQ5YS(dvtB^ay;{L}8G zD1pet0ryuVfx`}0FZS;ig62I|mi%s~s6iNOE4_z}$6PPVH)@~m7QzBD-c-hxjxzdG zhoR!4o?aq$Lz*$g$J>0z-ddum|r7qr`Ra6uYbcVxLtE1DEZ(JDX&X9I+6pGrf_%K}W?vf&nB9J}+ zn?MoQPc{cO39ov2Xz1r;+(R%H^Q5X3 zUy3EFSa{J9&h8$OpZ}at%cUWZo_s(kvbeE5JLXa8cB&ygYG|w>+W$fYj~toc{_+z; zDVw<7nh%6WhqTSp^tp|ifN$1^?&hlUB&20&*`=i1&_sD?g`@X0+cr3pM96O3Ya^A* zK>Qoa@K7k4((%dES~7L1j<0P95D3m@*8<>co94{D|%2c7OseXmB}infno)n@I? zrYqdl4_yIaytwwcOR`QjHt2OHbJio$tiLOeWktq^3CNAgdpHT+rZ49s5bx51>neL*zBrn=dJ4z15fIjtd zoqXcqUu_$|?fm@%C%wyjPWnihzHk1CAZ(yFl1JVw&JP}ln`mgV+}#bnBb!8Wr2GC0 zk>uuRl9Ys``mt}A8E=gvMdrK4xB%LlcA8LM&YU?xG`Ih)$&Ml z)7d{v7nu2?49i!3>_vZ|g-tBM^S;nj+KHC*rl&4aSaQ(mI3UQ3jFqY6H}+X0)!?UW zkP?C}ft2<~MzRjm-X1J+yK};#*>)C(y0_P!x9F>lT|rUI8mRDE?7M0?uZ7va_<;PKmvYi(bzyYt=K@rijr}d@+rrLF*`>RO3R?gT(t7Ea~eE4nm`5@ zQku)@X!}&=GY{p-W^#~bWOR`EYNn#%Qj;QB=qjoCPBS}l2sn0YDvet5;R z#^sIvNuuIi8)Vg@2slsW^sFr!ojgxdQqCZH6e+H+lk97V#mj<%sx0_(dNLNPPm~_G zce@tmZ8d{^M3PHp-@}=3DhHyeYGjA2FDH@d$B>GVIC^PYtO57rTALqA%H7VF{e9De zZv0fEX(>Nrj3NsqqT@~K%rsXmp@JrC;;4s3ZAKV}(EN^Qx|B3siaXOWy;kquB;qrOg16}Gw;0yY}F?5)}$JdzR!*F3;5<6C=Cg6 zHQuf*+K#RRXD5Y)MWhs!C#ygIuHPLo+ALrgzySN_0brGsc*bPj2Q%K#M)0yRMzQSE z#(x|owzh52{2G(_X_HxDYr&~?6TOIGx4TkUST@Fjnd+4>*AJqS1HcDLbaec8{Ec8P z^@kuUFCX> z@45nEoCl6Ip7u^l#*&S(ZwtRv4ktR}kfwy!TH8aW?l7MlS368%yn${WdK^J#At+{4 z3XwL_)abhZrlfOAPI8Z_#GOT{G+*^(L>b)P+qWIEBe_!=2VD*4I=WIGR3h5b-8Wqk z8yC7)eLm99a|cgEl$Na2-@igW8w*oNY&Y<%FtF*a%|iDhcfwVnSE3Z4t6(qHkCabe`6MvA^Z5$Go4Co>N)5E&WB{OAcw zn13u_q#KDHjK~U72c;%JeNPNqT`|V#iPqj&eSRYYrUc2C^Ve8{J;jsu^G@_y@_ZfSF zq+c_M)3rE+%+XZayHLbygQ3(-D!~B`D1NWe{_(f{Ap!I;T4a3=L^|O2AQBQGFn-X{ z1k{>X&iV7hG2TaGHgRc={6gImt*9`k_)_WlNCjZBOm)e#{r;vY^}rDJ?4Y`C6U+~l z$7aRV+r}s4^<5(`-^MWayr%d94X=n^$zXr>)^2j_{H`F)v*M-rYo z$0>VS7WrOj{ec=u8;}aRdkQh4 z(6HYf^r`(a+>DKh(-y^er-?%qGJph!yM)oH11@T4dgwaLMSQ2JVux?|VG<1=J8V)o zh0|(b=xlr8>OiB)9=VP8+X9VkoMo~OyUYM?i0(I!jl`i~Vo(K{YXS_v1s-r7L-h~8 z@k}RcLs#gw<#^%E4_+I?NuS{?ueQPYaFR9(Ur{P{R{;DuJ>BE{j73={%;bLUgio{dL+{^9=9oX=_|I|hq7Oj3j7qt)qK}SPqF!w z%S{U`Ut`_rpQL4Je5_}W1Fh#pGvjo7&-w3os4C^rlqvG{)MVO&Rf0qJ2U|#Kq2DvU zPj&LhE(7*N9o7o&7$z3&>abs(GDPzwY!^;KB*5=LhwNI_A60&OQk{N9-S9&VLAX0F zZyk)K-A~=<7K(*FW%_0@THr{|q_Zw-;&ENqD#=e3HYi0(`&44Ut7huD@$kXS+7?*G zK3Duw6^qphB!ACBgOz`G5_Tct`?lG>UN>F;K&t{+PKO;1zh`@>wfD_Vt{H^K$=J0mS@XguM-N~GaoLo&@v<-mW*Pm^ss7QGw7`7p|2tZ+=-Vh_E(3^Xb62RKR{f{jPgM zGVu9mtQ^YvUBuSq;!5lNKy73%-$IqnW2eN z2u_%@g{PK;y(Aq%GD{p;e!h0mf&k<+y-@-V!kX z$o)$?MusRg*@5`=iFalh3PM{+4fu1Gkp5;Z=vDaNA4DYLe}194oxtgltPJ&k!~UrKjm>*uJfVBKFBFwI3iG>F%sz2!rV|^XX2GHO#|I1JWWA)Lf>HWUlE23&(3~{!#qj zL<@%GKcd7B;39zgqDVK_yMmlRSoJ6T-A%-}l8y2*4POt~HW)=W_NHBEutMYCf)7VV zN;frC$799!lV}NvSdB%t1M{WySdWlxIf3{VzgNDAHezs(wFbORnfh(LBgDbQ{U=GW zMA4N|Jd%j^Z6M%ScfK=BH#?ld-qjO^n=PrGWuA`fhH;l_B^uuIuT~62D9Iv#7?XKTn9gJd)wgla*0KkCYkb~5l@^`ntT#%O6L)MjunTNjW96C$3 zI6?PA3xwq#saNR9nVi`ksRwMd3l^PvSuHgn^eIqotYvp~#)TB3HCy8TnJUgTq+%GK z$2*u$A8$~wM$$U^F7%@Xs6&J+{Y9tSPm|%$P;5|^a!+U=cEu`5QaOZTea)XT?rpVH% z?xcrgBERd!FDiY0$!0our791Y#a$U}uQYx>7VrDwGW~C|bT4k%EUxFqm&eP5oVF|7 z={(NezrT#fTM*=p$Y`3dFMd}qKuijr~ z>i~qNMvDt`1`v}Kp3QbeC;TaIuEFk|*VC1{Y|@8yF#MlpRNHrCURb@|x19pzq#S8l zp;=G4>$uCQ zvVU^$LM}RsW_vW+8)`H)H6J1Yx$W1u@GP*WWsYeX{GQbZCN00oCiZm{aL-u47n`E}f|<>q=(0ep3R-O(Kq zciO>oqA|gm_X5`&NgSlxyt$^-*A<(1wyAJBsUVQ8QYf!c`IVfPZxQJImw^|2jGe?^ z`RvCHw_#veOAQ9po}sq~`(xQq*Q8wL*+&;o(?e;@?|=>q;jmtKtkR!+Z1_~*r?|h` z-cDr=jEcR*dZu9an`46xKNuSQHr_w86;URG2~q^(<+?n~1cdAs{ujHWB4=)EOZhwv zU*`v^acy!n!sKoTED0mi+ujhm472T2$xb|IsAG&N)L$RJ0eM;h^?EKRwvp zo*K9j(1d^aEi=z*HllIv^>`)`DdJ`?3URntj|#kqaOfl?k&}fAM%}^av57jo9*+>S z$2z^W&FY6OHZFVTB)2`cpP$!7Kir&b0*;&4UIwXLwsYcb3bL}sq;oy<5rmw1L)juhbJUlym6?a0-AzfO z?SnT)z2ShD?!jJXwnU~VxbeUW@4N5@acV}|nZ9mvg(Oe$Rzm=c= zDMhT?QW<5HdZmx%St&gS3M}A3Ne|LE}K3UU`8bQE;HNM*Fj_UGoE2(u=q6T31 ze7-+?8;-|4QQ9a#-u~l<)z4avC$_I+WL&2Gq5Qo#L?#21D-W6VtHK&{h|sVjVzkcA z*FUDln-xfJ?;^zG-pidHYun;8Pip_ciSBsg_5HOv(-|cA^s#2*2Grew;Dlc>BEY$XbjaZnT5)Q|H8`QUmUE|wu zC`rTu<|vccmA2GcGsFY|B%!oK^b zAmg+66Cz+SdgS9(-Iy&meVZShFaxGaaYaS6-P+3v;TQtpfWYma+k;iSl>RH&^9VL4 z^Lpp%_>&X3(vMBGkG9;D%%23!Od=O0gP_xaJ*$DO4l=n|a_ z@4YDoDO;oVOD9z`_TxG#};J47(P?^ZPeKxGZ|d zfpaAJvZKd*c(LZaMn%Ou#C^G?%;*O*T|eS*15rtzm4)-QH+nV3Kl zKnv9&qO@@V&Iw(ouM+b({g$`#@ZbfO9Ia~dv#mMFSa?K4{bRgLn==$CP(@iZSa7l04E5oAD&(b` zWN;cX78-CUMLd(=^DqOO)0xvvYfBbihzS`6zGqBb-;>IfoSeKg(>PNpTJmFutDMqr z{-tU&CU}B?M;$~l3k2|Vduaz#9{M?(1y0J{=%a1>6HeReoqc%{^G7T@({NZOcvF(m z>M^{{Zz=ji`nX=v$C-Ql^~6oXHb&XDpS3OL62U`8$v}8C#+fhpDiV<-UA=OtRbEf9 zhx3HCGZ7}TX8yr05I9VFhBJvhdt+6(bKH7*>BILF8Sn&)KX*z$RIv3XbGTia-{RYQ zpb&8dug)@2seYUq3;+UD&#~4wq}v7dB_upX32!ffoDb9c&Oeo>UNYr91nCwfom?>n zN6v>tM=K(vBcbT|CqN6h~XxDU4m`o%VDYX*tDPEehbjx>E^+gblRx-Ic$!11$n7&JCFsn0Cb;^+4G|MNB-r4nM2@u|XIBD+#zG@x9l{8+Yb|87UDbAGZ(l!62l8zSy#)qri zyCFtZa~hBy(^BgV{LQhp>r02xqkh1Dcjsf4$y?rzq`p9)=eZbUQ0(9s=U z#&zc^`R!oHwPD+2*$_>0?t^CmNLS-A9pAPzC z(_p`(98uu}97dh)DiIzD4x9I~Nx$8{Y)x(&;(~ZL(JfmbBM8|G>=dA$^8=*I59Mk8 z{IN1Hz%}z&{aA735egQM_lQiJ*IXG8zDr}cnj};>fm=L#=JD{gO(8^U$)rBitWL zMOEQiy>locj-pr;+_2chK2d6Yq>-UVF#mbc;E$DJ`#t+E!+5uR3mBX>CC+QJ! zB}aPxy(pz|emv5Hu;_O?Z@o^)!o#mQPMS9-j;bY5XwVuQx<>_US9rZX1$v#Og$CE} zPZoY(d(7_hCjsj;+E?+Vsbi5`a@g^oKceuFAdwrnCT!~%wK`=BD@T724fnGdOkhaj zac1n|n9scQw+~t%ArS}mCn_T?XqRcqziF@}pIAXi_~^DF+GiWVA@g5)@3q`XMvI|` zQSZMi?#J)oo^x(ib)ytbmt))q@lh!!_3|c2%^0}x^Od{R{<*$EQZLc&6KS!_eSAa% zz8$gBP;flTHI&xZ`}o=o9!(v@*DmyqTu?5QueJk zP}v4_c@fp7qdS@yby1#1{)sgaM=@iSa=nC94#RoFY~RLNyI848y%(mwCgQOmg^8C993HBP< z&w`s9JPa{M;nCP=xX1Agg`q-sVFFPION6(P#ay;r8){d=35e@rTM}vn-efdZ@{T0! zlHF!(1Z4a3V#%*}Lap&iD%g_fBkY_lTw7ewjNKNT$@_LbxS+0HT&el2Bw+VJexO(g z+f@rUKik3lwKe9!(lOduSKaEw60w#Wzb{t>-ZX`1C8O`^IzuL4`;6}#LV_SUsd#%AsKmMO%{__#e85R%;%b^l(7V6 zQXr|)*Q&MZsnj~#!3AkVDSnUV(~!(uME9wbhFo3@ z5qF1g$#}!aMPI7-=+%55=U_k?zvTM(R8_m z4;Ju<*!q^zdF8JD^?X4nt=$!|7$y|XjTbAFQ>p0j;^~G8+GP6A^il^-xke-6v&!RH zBsv9~Dc(n6^scUUUx!_T&hC)+hg?MZpUSPEWuOT-XYg)b%uWuCNdpJzHSi1Pxyxi>2w#z$uw=4(iaYlf#J-Lb5 zo5W#j?TO`&aR|AzW4&;31o`GIvkgBvrx4HPyCrMafC-*Gqs+gHFe#`i;H#FZg!(Dh>(^<;<_k+69 z)D)3!O-|T-0DHU>Fk`WNFNavBGu!x>2q??4XASB>QZoIT>~v(M?CScRqm-%IsA<3M zr&d7y$!r{EhKkl74Q(ectrqPNU#^S>G72~DqPf(>mBHT=?T#a(>Vr{zwG=g4z2im# zd#CEFd)*=WYU-F7%Yax{LL|IQS+hhDYueTr27%N4)iul&AO5!qu3R+F{`n^2+4z(6?zV8)M z^Xuv^N|x0tSk1iR?6R_@v_oaS@pFh${aSM?y9Cc}pdvmO$!g zkXLZ8ZB4sS{PYm{Cm&aT((jPBmHSKJW1IQ#s80FM39Wg4-~iE|A*D2d8d41_eI_+v{#D%!FKLPZE}x>W`c8C6U_sB?#XviK99 z%vjVSKrKP|)|tlb^p_3PN!fKz(v+&G=%E>ZbnL{9(DnfM2cl=grz?GunrOp-JssQ% zzs_M6`3&AN>&1cTh5N8;)#-t`%l=QEH&4uOARu(xZA^tdO55h_0AIIqN8g?k!cv%d z@__o;#%N%r5!5uX;w1$EA*i@td{*VfBhCqBS7kqG@O_zWMu2TX%*y;yFjdRjY&3J}_4RmV zY&?P?9rh=zIg>UN74<`efN;quBH6gYWT2qHP<#x@C|0e&(>i%jy2aNCk zFC)6h+gE!#U}*me0HsET{@3pQn*2{L;(sJ1#A3IKApRc$p{>nPAwMD3xa5D#`*AQ! zw4@`}0Z?h0gJg08S2+$^47mbsRRTZrS;vKg;NWQrpeB%%nU%XZyIs&lf5|fU{GLiq zG^K3}^rdKH^W{Oiy;`calC~t+ZaHdOJz}+St~YA-&Uk9r<;DZLHxLmWJ!TLI+=dgj zNZS3zC;J=~h^@P~S4C=4O{`|L>8e{+t@81)RnFR4eXF$XIXsf+PUzdWD~ebD&|#Dq zUsM#uv~MvRo6787OQJCM56MyZOg}W4`w@w;-V0=|xE)RRO9v}V(kaDFEiMiop&mo5 zjfnIRz0;&q|B+WC+zh$_u|F9ZCDrt68YqQqY^Y34P36fX0--9aF~0rWiO0YYP_#a> z{i`oxY4*a#T0=j3E3YSo`<-6ug%d4TQ3bENi@D|wtTkl$_M^|mZwhtL^ah0 z^_X1oK@7CDYd9B(uZ!6mUu!^S*5&0K)^4SY`Zg>7Sz1zBty3=CZ$+6*)RO(n^WRKUCwkPe`iy-C`U0txZc?8=R?M8W`wc zUw4xFG%)h4quabRQ6M8%1_6<{q3ABLYuzeN)u12k;kKL`L0Cs;HvGvU9BWH`VDSKD zb9a{Dqe~J_p7GwWl*+KmFH_n2l+mj{>vqeMu3Yr8B0)%RAR3b3{Jz@`D;AgWm1$P> zmpotlq+w%Ot4*}1VkgGaxfQ6HZ1dDq+edlvE4?%0X)9qPJxFQ23Qak(hJe7GFy3p3 z5TW`t+lNP{qN-6TQEe1E921+6VuBREoRZk0VX}w#`*>0hgiyIXMJ`ItD?>vyjlsi((ZIQA%o!VxRZN?4~Bxk_rL3=H+!`uL;8l zh4H%!+@ZJoDmWhIE2N2)g6!;tqN1W{_ut+bkQ?lDTH9aHBVJyp`K&&@ z-@N{Ys|S1k@O)H9<)fesct!tp&3x<7wg4mv;luP|IIM7n9^Rw%crQO2~7T}V;=-6@=J#izEYLwrc z;DMVPs&6j__;IgkwRw56vHv00T{>$Z7B0(5cN`S2g7EnRy8!7|?D2fpT6cMUg0aP)JB0NC+ zFZGp;qX=GAkm`Dbzo6tJ%BH$Xr7z%suG2uHq~z`MIiW=|A)#^REj$>Z;7b7o894<7 zJlA-sKbvSgmSW7?w$lQNU?y64$bjrRVZ}lb-w%3A4e)1_I3rW@Dx-=2Z|vb-f7;@Cg0+hz1UM@ zVGGyjsg~=tMq-o4z~gXv?$CF~tEcsaf|Vp?WjmIe)Ax>##Xrp$K*Ln@`hMsBj2DX7 zrKm~*v<%%MNErZBq({RwYNWTdglj0#IG8-l}DrzQRRR3fv?+YiQLmv*D>wZ z`O8xxPj(Sn&UDVi{5%zc)DhUSsM%E}8cZ`az1%(FqXHL5N_U_h+6lvUDJLbS-94Y_Z&;OQM0#{1MD#H`yV7BL-%N>CS zuLFs4tUaaBeESI{4g->ZKaEk@i{Ia7eo_SxS5CFRA$J#j{D_9p1bYWV9rKe8*^q$H zyD#CEm`nZoUAb;E3VCeH;X)a77q6;n%Dsf@IV}h&7d^+&+5~wNUk}eFi%v??=@YXb zFG``XFaMBNL z5!5pRi^hY3Dg}(vI6LO3%x@?Stt6vpU!tbKKe_M7I447|+*;gKOG+5Y9jCQaGb)5v z#)WAnQH5nyCao#06vX9iNNiS0f|9DKOeCe{d!D{DPWl$iI7RH#{Q@_AqF=pbrPyFB zlAGHBVX4n-Msa`x%c2msIi=6!<|p_=q^<5)wjNVLck1NNTKt!S5IJo)DXxTxSQx2F zBF;BBbc&j3u0K`J{>Udz2Vx`k^MPWs=rOm9_3sf%5tn`T_PDGf)H@Zu(fCi(BFeNU z(D19!9d>Vj8Rwxtda9FAajRp2^Z=!=Q!)2`$WMj&Uf@x_PBs%NT(I&^nf{g;GmF&xj1k^qW$(qiP6A9Q6 z+!Quz=V|AnJCu~Zv1Q=>j{n?aanWXr&T&){h!&xpsegYn!vypK?_70nXhsH1v7D@C z)>w7x!dNx6o0y5eFJm2*@#o!}WIQuVrv9J`g%9yx0`Mr~IxJ@(OOZ5#m>@eVjnC|n zNXSI2WMj$L4wwhbH7Xn;s%pA5ko%Nvd}%Y{&|K$8*XP?Tsz1)6uB;z_U}c*ux?*Op z_8$%6cyc_L<@$|;wc(23nq?-#|5M08-#y_ZczI#Gy3au@zw8Lb!{fUh+Y8tEd1Q9h zn!h7Gg#}b-s5BbUPuQy~K;MO7|AjO*}GB?B4|5gGn7YB&1@+`09e+$a#{5R(> z^{dYogw(5y*tUMCw6i*{mZim*mi7-0hUsZQTPWgRE?@@eMdV6OxkfOu{t7Eg zU>X!zvKX5BK)i5wPXLbK;*TD6;)|KSlB>w0?hgTOMdNl?7w`kL|_Vi6Z=mE zUiqLX4MzXU4Co)u^2uo1VZp6QqWhJ}P>$7*L+dnw8@RR{rq$?#>F~Q5BgPV|WME+W z$9inDOsDhbY|+Hdl^uoz#>kD8j+GlmQN6la{o{Dj)_GARtIV=;vNn8F6*VET-(V}E zD*_pipw}FCSP*dczc}t*Zc0_ld(Wi{3h@8oiRV32}4fjHW1gb@-hRr!%i-b{y{#!JR>Qshrs81-A z@;|wPV%Y!YcLVu;(#NQhFt6>i6GSZX`R5nv_wRL%RZ$5bfc_=G|CeEb^`CnDx0n9b zN)i2^%Ny)kvn&drudx2%0t)#L0l2;<{{LxAwk-Q23H_H3EMd$`GRhkzp4;fW&wT<2 zqbt4v7v6l^Hff>3$xx`j zy0ejGGxp2G{$uEM$IYC_3v+qJOZVpw;i%0IXN^znPTH9{tE4^yQ0_ORm?p%1Z5C;49If7#Ywjb}Al49wf%WpOh2u-#=T)vBj`4R~C~6Zwz5E3*BWGew9Z;rxyR^ zD6hC^Vfje24_AC91AP|x-Fb9(-m%J(^@i7JarvQZWz$K3e~Itm`K;yIqYW-wxTvyW z1RnhIb-J4rvsa<>z5$Df_cQ_#U20=T_jf;I;N2;H3#Zu|J(h@`>=q1TQu3 zuR<%+#etEelzdR)uAuO%PP}qfXVRzGAE4qzXOWWoi5rbZ5K2ESPfW`<>XeieyOKPt z+45x{RZUH*YqK4$%e}grUkldG`@hW|uRFiV&b|Th{IneV@i_M;EiA19ALKf4aA1G& z`M`HFGODZ@S3sv?VjKEr!!w%OVRReP=ET9FsX7l!xp`pdWwhSJB>A7Y^0+LDcRDN+ z>_+#gbBRh>!o~(ueO#K3O0Au-V9nsuV%92to3}d>UU{86)wQWHsSb$WNe*TU)xRpD zg?Dr?rmy`Xll63F!Ep4EHtw{7zK1n5mxP}|z8*sYd%%EBZf`fZsYx;od^>_O9 | zqZPY*D8dOF$r{&Zy^SZa&&uhqHQ_blNu71g~nriZPi#GnZgH}2;M z#yR&(-_Qinp^QSoo*ZE+rz=>jE$<-oUUZF`uQ1mq)kW$mIXLvVMKn>=pGR$2c{BG` z%{HI*lyW~4u=eI-;t&KTlgf(-$2b*@i~U2qEqsOroi$T1C9aR;7dC|_4qz6ITMYDl+RZv) zF*iwuy}jtH^`4jaMOtn5AKK@>LA10<5ZuvJrU`zuBmi!>e+AvI(1LWEA)L$L+Qd8T z4xX9r>jaa=D*}5=cA~ae4_IRDQ*w1_IW$bQm+vFIq$(bgF0#@1kJvB>1%iYixAq6^ z$gJo6fv~P5#xl*k<;ThztyH+}{3}SSz)aiGA=AIQ7DjE|mNT|Hd(WZgvV=!CX#0u9 zCW}8;S02f?0zE3Kq3it+iSQ-Pz&uK*7vy6eLr>!+cPjREsZOr$gp3>aR(7P!*!Bu8D2BZPXy-_EGRpcX!a% zVn-}mM|bhY7iiiY%*J6qCQhh3iJ|z15{zG4Lyg!SS7Gj;Mu~uR?oGl3?~2ILMVr=v6 zvb)hG-vPtOslX&U@M8D8zTH3;g@zV@h7BMFEj>fUzK#?B68=rV_&3>KV}U*_CL8wt z(Hn0DUAh>;uW%x-44nN3?}k2dD$W##wB1!|G}llLJpBDh=(ckgy5-u3bhy7IKHSEFT`$wBy7W9-D?Igr+-+QYSmw<_WHZcc7?*a&#=89+TR_&9Q#1M3Znl0wR^b#h;lpn zCnp&2mte0of6MzO$JfS-BQnk*W)Bn-`Z)5x= z(qBpeD$Tt4TlzoU{a-EiU*-cUc(TwCph^XT{}Fskiau#FwJYAs$iYFDPd@W|MTPe{ zN1UvQXd8phz9%;%V6|79K^MukjA}&J2X2<#l=_YsS>64eq~+41DU?oSxZFZ??Nj)J(BXc}cY}mAG+^p4yeI);Lr5XSXa> zbzd|T>rS0^%9Tkl7Xi+X{qqQ|&b7RJD>`6ATBGOCt}@dM?S2~!e`gDqXHT7yB2Z9d zi8wM3R>wT9rK!)&f+-wF{e{h&=(hr;o-$GIZ)%n#xS2YW?-=B%qwjqoF8XcX;5(lj zH+`(Pf9^Vaa?#ys%RHLj`nq$yoiP?P7WTjo;7uwaPM*chHJag(Z4ty77M#~RtJ+;$ zubsfF++I|KMN`r7h2}u^o>Ev%v2M&*ng?B&lwl$Tq05V;sX`|_?PuGS?fN(?R2BJ$ zPQ@0LsM-7QmJ5LNYQFObE*yRh&H2?bW^`2I6M9I_zsvHJJ;70lHSycqF9Pqp@}UKd zL#eNWX&yS>|3?&EHmLDa{)uIK%tu3&lW zJo3d1kug>%7<~&RKindZvv&}gClSZx2bzSZek4VgqS08h=$Kg92sBFHo%Jf4A&tao zxg8NFuC54C-mI0%025`@M}q=h-#gk3y8)K-Gv)5U--QWd=clG1`W=c;o7uJlEcu6ST7!|gh` zR70ahGn>Rc_>dfsKW|KvK+ZF30w!EfR$jWVyw!k1v|CshW$yc%zKCR>&4P<>Z1(gs zCZ1tjHCQ?F6-kv_xtLV6jKoeG6j7#eRy!!JK`ka+-+Qd}|L)gg;UO|=u? z?_8tI%NWU@No6ALuC8(@!~o}A5r8}4z-eb|p>Br_GI}*#Us2aPYJ6&*K#882rO-Z# z7Kw$FBdUs0p&UQ|r{8T!zYLrOf+{8`XD7G7Mho(2deS0utCD!GIUhq-MPT=}3^y=Hqvw;KrZuoHc$T?E@aE;4UaA>%w ztV6AYIC8kK!8s@Sn}?=k78F!mVn@#e{-%u6Z`h@W&pBvCt>_kld04Y74x+q?)IP7~ zLxs#ev;C453u(08oDGW*^7{B@qU8P`9^bsN?w)QB$b=6sI=v~w7FsP%%Y1^mYO_8I z3yVJ=?MO>i@l;EPW5>n~3;bDVbcTy|oetENYwQn_rps-dd>Xup;p#jKPQpXwJPQOs zXq0Vx86r4*UueZC!uz6cv%jUM1VV~Y{+hme3Gy!L%nMOA78i?3Mj9>Roo1nd324}| z5f8YV11*`RDUKsxhV}Yo%Bu-(7)+DLClH88?V(~M;dh0cXA;@|mL+%ky`6iWh64tR zi8N{dEu-fWzhH;av&DYntC@O@TO1w@{NzLx%T3mBX=63Uyso9QKt_6cSetvK!Cf`& zoqBsD;K6Xp$`{#uTUpJXX#bq5`qA19_2?$5tJ{*CK|)PH=Se?6JM9h&TJvOn$*&=mXsR`==}{-^NVqA=T;(N7m? zB38-&pRUddD;v109@a2RhuX5p^8=>2;_Xti?GK&aw&3mEn7Je`2Ir}M^lLkLd!il%A zl`wL2837P;16e__&Fa~7*2=Jic9S!*M#UU6%#1v!2lYGMcgM9~3tCfQGpclTGqPB9 zQc9LMN|Dks6WA8Xl_ukpk_~0TLRB35_n9gYXlV56=jWpjH0O>NgMjm>h$ffJ0$?Ez>|Cw0RiyvIFW?=@9by~$pFs{4N}4OY3aYb8t8vr2B69R ze=^1<2^$yS|GI!X9NaI5N}eKdk?*aiZ|52*C@35@L{o9`qDop?{^K4d+up^-);gRq zV`8@Fo7(HnT+Qlm+@(k~nd`*1Ii2vyY+bnh<+K8pOsMIAlsg~+5rku!W%B&xs=rCUgW68s zxQ}hQd;a?kg$m~u?l53tSSgyCCVfFBoT$o7!7P0g*4AFGuCjD*YOmnQvFX=u@OvuE z)~oQhf-XN+SPSj;SPGK`GJm2MMZad&zBtedtRd~7uNx(I$t)*gr68v+W6GG%#&U> z!@NA@S-GlaFll#sdFaG}a;~FUHY908c`0A#h{}$^W<*Aof7Uc=&I662t6@EKNZW5v z%e5yhY0vJJ_dUiG4FKwA2glV&U-L!pDmN_$NekZy)W6Vulo z9ep-AU4y5^O0f5s+(|yYmm--t%mu zj2aT=%+!fV6-Lu?{+1u}_9kP!wu90dS+#<<7XyvvG3-#8wzx!PL08pvwQO~Dj`&2D zQI-{xkn6J@hs)ZZm7VeX&ceE^IP#pz5vx>jQ=#Ue0=RcjKp2HYsqR%*#*L1cL7$fw z0|sk10u4NIQW?w(&C5VyvqC`z%h)w#iH<~zY3{`iEen@9ytM9-d;xAkJ=-R6%H^^yIZ9sw$>@g%AB|*Q)t0BzUwk66#H*)U zG?__!IG~`kH<>NfZt==CxIt-2n&TrUt$?`OD(?++OZKQOect{xU_} zKX~suxK8@h8xX17C~*KGNGod;>Z^Vx*X(=mat=|O99 zoJCXhIS{UIoASxBu@*EgNoFQS5m2W$Z$a7nT$hrBf%{&`i?Q@Kav?Xh*7S!AC8?h} z=7WG5)RE$KdsY3O9l3V<^bUjY5Hu0tUfu=#EMg}Q4AJ$7i-`m>JO%V{dFh5cv$-ot z3XFNP4IMdUrnB>RQtp;KYGnZ*c4+Y7%tynEz5}XM&nDMxKQ|AIr$r#o*BwC7I5C9C z4$6*(hJ`XiCLF(KFqauY>gAPR)om;)FSo*xjbzJ=^`tGW{^HMTP4%6Hz;d{#FYo9FaIc9kLFwEM}-ZhAw* z&%k}>*%|Y`OVeqm<7xz^uFf9jTr@rL-uXj8pLt&ZT;y#uiwQ_cfSbDoq_&#H@JSOh z%r&%sliT~5XWDzED8szX;{dDY5`l|FR(=$_;O1()0-60p$x!4w5+P^#o-vCGbCsyO z(|g4ptg;epSB?PdCkUFqsC}ddgFMM?OLlGvFo6vVD4H#VB2MfQ1_edNBE2leUVCsa z3FsmPB40QzNSenrQec1Q&;H4+vD;tEZDP|uV>%p}!hceK&%{6(_$xnPw*H&*|44a& z!5Qi6_`>zyYqBfDfPVhjuOUx@GkW+7*OYC9YEtu_tCvg<5M{oCX1!6&#RDtI9rmw_yhC%NNtVJ1`FnA3v!-IQ;>hN z4}fsBw!5y|K7{6B7Ys=IGlkm0(3At*|LXsh!2X}it3&ZmzYzbG`e&;F=q#G$PX-$8 zzfynuCqNlkg3iCRfA>Ze{ZGF@>Ti$bPwIci=|5%v2m%=Q4M6|IG|0|Ec+ZLjKtYqln7%m7$;*WZu6FSq0$$rhoIf zTSbokm;e8v!CykzQT!3-wRJqpANg{?m9lcu-u{sg5#Zt*`ZvvgiTwY9&gi%Ph|QaKJ@*}TdJSkZvS zi~SvW-o_C}P-la;?r<_eRD3*w$K^WZ>DAdQlYi8E8XOYv>6J;`99?c{wB5AM^Gfm^ zN~$ne@e_O3%8T+}l#BcKp;*iuI~#&sQsIPLFzfn?bIFddT|&UG|D{{)x0Ydm0{@sw zYgTvGnrPJ)mU?Ky_GE?caHZz`%TnnZwcj&Gz&-+NhEsY`dH>wcxYJDcK}%2W2P`8a zUE3MjrbPc~6(;ph1UvPr8Ij_aJsNYgQ4hswoNu|HFdQ>U~-YT5o zF@j}xt^ZU{4QTb0I9uWAiB4>W&L&oFdcSqd=hrm;c6YWgX~@&l{YLPFa9L|A*WT0v zg%fQR;}fKse(j<(O-h(l$mXws@BVc`^mFOV*ogV0in<%^R__S{=7qdmisLwLzEFx&5j|cG?}{=@~-Nsi}?(n%WuwHgW5= zuEZ+f;xkq2SX5yRre;+R*(;D#*LX6VY|m+zw=`jkl@O2LvvCPWR>5~%A3Fmgdg){` z9qFDRIV_#~Y!G5G^l!t@B!^AdZN*!&oV|WEI9sK4d>n%sp9XOR z3X6-2Rea&}^Ews5Pm+JIaj z8`)V}gguE(6<)qbzUm zFu5|T-J?tgYuL`aSE~CBWA}5ENN|2Ai!vn!nwAgr3cGTC)0aBXB6Jy=<)2s=g>(SNGelOd}`- zK1sG$v%aMK8SxWy4ew520TLF2HdDf~&YrK%?!bynL^0NkP4QitoARtd1=|eQ(U^eb z*#cynts7GU@CYowJ_227#-=-UxC(poZSblE#%Lf~Y>lYQ(C;y6wQ#8%+j_#gtPZtA z6z;S7vKgEN{7*XeD;#c9*dFFxGFdNBUU-wj#LtNk!x*@ic{-Jje52ka#ig41^$$-5 z3;213#%{y%l9HT#^50B~_ME(S|B@@K32VkhiRIAsGLX-zYIf~RIXdl|+*xyeX4cMH z?`mCvcjr!lj(QJ~Y1wg;;^>00PO@*m^5eQ-^VLwSxRFoK+UI-+Yj!~!ceqT8K3nUHAwUR@|9`B^9YK^POLb>j^;(*QZnx872ronhO zh!NTX*xysmD6z9vBW6_gD4f(-H~%1QIUU5UfVFSrsEysb8Oawg%RTnWk01ou{}CDQ~%?XmuC^YV>LhaS8OK{_^hZNgYB z-M+)vw>y}jgWtDBZy;-?$fEmPNW;x+eADx5&Mde-+YR`38(Qm34h{o-;}6R?4exSpTHSO)f@*m zpwZ@$%6BK6^6fTv=iwRR%9e|iGw|>8_}ES3##Jezm9IQoO`dDz{PYqjH(Hw=;^6jf zl(}G(KG1S)RGiGkhI%abI_5UQT2#^&zUB_N z!fOH-&j#2^&aIE7c|mXU5>YX^JBQGeV$x!f{5a{wHjd*UJIf7KMMno!06@M#Z$az= zS;T-sHKM_p_?|E7El#A>_Hcryzs8tOq206F+^bHe3Q4-I9^{!W zSgDuA);-vr!ZPD4iq@x@xt0QGW8?CEJpYxPj~}zq`?Xb@TS6_RWoSAf_Km7E{O`Rh z54mxWjKy^&~6wF=S^PQ0Ub)Ad?sf9#hY(9z^_wc`qLX`A@4jtTpZv>5qokoemFB!54KI{SU$61$HXp6i6&Q;z zT7q2Hpye&hx0zgT=7R9BXH1yy65!JnTnt8c}mQQoDfDFxhZ$aMv$l((M+P zDONY$8(GrJ_+>rrRz$+)_d>q!t4Y&1TjIKGC;8LUw3)JAfM2jlT6t!A*q?XSxVTWj zl}!y)x>OOoHVGzC5`F`?boJEnRZWhbIIY*QXC}s_A~wJ9UEBj~IJ7L-u$Og@5$o5l z&k+K(zt5OBof{Ln64~_$?;A=sV6_;gSr`ODjiGC|3>>J*zVA(CT!DxGO^L^ym;cUx zgb#nQ4vX)1XvA;nuQ{am?flqesc}n?>dFRh`E>SYlxL!_=30LUWf+o0j-;}-Ov8@viV*5*~l2nhg#X(NX>h%8bnOf-??n?933Q^8K6ZB9wN0T zDL=+!Zxm3LVY#)127pJKwRX6VdEs-~z@oDJuDYs0sj*=+?zcESM@sw+Ctxsh7%)dR zf~lHk?0=-a`HpfUHX7r_#|EFz4PiYeD6i>PF;;p`rR4*C>j^)!u9stXnkYFVTAD7D z<@ym8#4{p7GWm(_8%wcNRNJ$+w<)s)6f~-Qg!MMzP6vP~wsf(3s`4UaB>d3P`}o?rd`6_Yixz{72`B;49(fAbA0{Hs_w$57k)Tsm<_*J;ijaY6 zb2}$AHDFaZoQSoki$^^!jWGY7Jq?XsTS~wqCm%bO8)G4-VI!uo-{}n&aYs9oQ4-v# z%5&gjX1?g0!bBbuT&g9|ddDz&w$hgAv&a2IU~6Uf&UUG)7F&_Jv%lr$Ex3%3l@9Ul zZd|x+(6oi5+yKhCEeu)I5_DvIhnX1{6~hax>z$ZE9GN&sbS#1>Mrugi48>=z5o!np z6OGYe_1QnGolFyp;mNPa!_q6~JUd#->1dHmu@yi(_7f$i=0f~|WS-}R8cq*KJr5q4 zZnTvdD}EIq`WuOm7a^7&P2UfxJ>haqs5x8T9w7(Kgp@|PQ0(#EwLo;65G5f;nESxn z7)ls_aa|9C#7QxQGqy)i;#RET_)*6w$P2G8G6VMb{NiddYI^&HIe(Bu)c76ear_oZ zrQ>IzXI0wBG2zB&CT?o<)r)Pb_G0||Ufbl2#ts?NK~8K|CSv)~B@Kvz#_h5ts7L{J z3W6@*8UUO>lGtBj?yzneO|*BBCD;@iXFTDi&s~j_M!Us>G9(ckYou4AvQY-oD~DW6S{K3~6HRI4f{l;J z>4^Me*nGzK#39n^w@bSGuOq$)aK&OK11bBEmO}3b&*q-ReDI*vrAK~LTBt5^>j`;7 zQcUKjy`ox+1t^absk>n~QR5W49N{;;=5{Cqt|26@8+W5t8?Bd#5TDgp_5}O2e#Rk7 z%kcM2kk7Nn*1?sk*nZgw-r5qb45$l}!ICoHBc`UaY$By?8Q9EI4MKWmmqEdd?E?{q zI?#KziTvog>8kZf)^jLY-$b^LOwK4`42ZTrin^Hv?;mjNMIU6D7_ty0@4ghx7kX+A zj%V#w1olOzdQ276973W-IxDktJAbaU7%t%$Cveplz$sS65<;W7ZfWi;ZQg%2Urkg@ z{4>!_eJ%PbF`SSRfwys4?jL0x%-JsBJB~SHTdJWJx&5K~v?{OWM`+wID_MaQ>SJ25 zvI#kj*&Y`a^(X7_=`jHe9w$&|&j*win(N9He=?tDsV+;ZZ=(k6HcS3N5s>Bt!T!Rf zf$ePIKkhDc{ad;ou00_NYPFgja3mr<0;!~g7U)q!#tkX`%?wPww)C^wB2?hXwTgnm zcj3%)M$`D-6tq}k+TC5BhH!eXlgJ_vN2*09Cm`d6FQk$Y_x_l%Z0Xd5t?|G5BqFBo6bj_>7(gqle^r`?IZ0Z!;y}7s@prSdse=^ zd?4%dK*fBQ#lpbHO%oy-5JPgDCLQ{TwkS7Kp6XklQgt&#=^2$yTO1p0!KD9qOxb$^ zkxaFR>}<{soeUvy#V4Ofei}<^_^`Z(fs=hSyCD{h>*A5Y2NLgsk7o|T&uY@4d3prX!DYi z`l#<8!*5bCrhE;{CnnUKzH*stw!*xdBNVU=y-B&^h4ET_{Ip$%SW3Nd8_{jQks=Dh z7TWNnuw>E}qpS)zd$MoE@DmMeS?z0^;pXsnQ(#DqoZ&k8su;LQDUUP4VsTBPv1Psx zU>+>TPl~QO;qM*rOE+8F;-F~ZLe?B~tjMFQmV_1luB6rE9v$9fUM`{cvd#Ufr}Mc- z&zh&kH(;-R`DpNkE&0N@cIRk~H>qbui;*oPTNz&CMpwqwi{u zFM5p5Ym`m-$+e*O@)dBkQfN$(spC{4ZBL}mH-5aULi}^M@=F=@be}xIJlpX8b&kJ< zy`PHL1DDEC%T8MLeE#5l)i=WMVKa9Fpt%BYmGZAoP7Z*@3P(=!=OYcmcx5gs_SYxm z4;X8F(Vq{{0w4@P0|Kf1Z)pD_#gGR5`NS8!l5VUXE>|IT5+R5Qopg{u!@@8P!f=|A z*vdEKlqaxsv zuVgF&CUDF5KiPjvfw)gnK;`Sdg~RkW4p_bD{fdeuS(k!>ux)|w+=Ll~h2QKE{T zOIyQ9qf-l`VC+#%@L+2S`~3P&MC9i~P|Epo$7^?EcSpWO`9k&eu$6=1W^r+`W%+P) z*xBTIG#Z=+z&vFKAmD%m0uHm$PcY1$KP5h)#3HXHg1wdce@X;&aSb>V90aRQR2NR&67M&>kL6B-^kcV=CUuH?f6C#kS87U*ahR1NGt3P{`?%vV>*g` zU_1~ng(i?7=~95DTu{3Z!7{qBPCQWNCBCr}M;=#tA0CxoRXm0us4pAKcfs0$-YR6w z$u5|x!b~R3;B_@9d32qS|FxpQ$cqVptCuBgHmNp=doH`^!Z%M&Mt`9Jw&!XJiPr45 zeiF~um|{g{da_qpthCNUATcSuJU?w;&FUt-8IKZprkeh)mSi%LG9WL|Yr%T7+@b~` z#*!-lAhFv$$O7?=c>px)I0sg_Lbl%{08@G1IDXRcLVqw*7WYA|9QH>IA(x#f0*)I> z06_-a*hx>z?ZIU`NfP#y0dwsNcP6R-bs)mEzkEZjS1F?%F z4jONdTTZ@h{`|^mC%P0zpzBFvc;g)Z_DwVVFq3hbeNy|=^{18-{0omLpHf>-5+Ar~ z^M`8!yR!*3J78t)JEnW9v_`F5Xh0-zy5O%LWc2b3iQlhY;dHcMx3NY2md62>53S8N zhQBQwmrc66mT!UE;Wik6*_4gteF81HXka=Z;Lj#SFm0r^(MZffjOmpf$YtL_9lNv=@Wx;$)z{u zh*280x%Or<_UtVF`ff(da-?yB!e*mx~l?vquidD-}D(MY#06f>9%Pr2i&^*`z2eTE8#gT!YX zKszSbyw3wm7)fpT31EJb%a>~@4Hm}h5iuy3P7Np*#@N?Gcyf39#vpas7`_MOuRk|>GbJZ} zjBzUUXH3}Fnk~QJJRQX_k@jDED)ZlH)e7c&zsy4E38z;Z56)?ofT)KA*D46iq?1|S zg3)Z;O?*1Kz`uUVj0ol>w~G+gx0u_;nJB1R^we40Ub2@buU?eTq=-<6dIlFQQfs&1 z;zIKRIUeh^Q545h=`4jx+OB*_Wy(UBvGr7)XuHkHlZ_SSyb&IZpZa(T;{l-uO`NfD za6(EtbDhXBdug{0{@yXsBfzeS6dm+@f;J<76sTM{wicbm+uS^?_`6`JDG(8Na6Gr) zXfVEbSfnM5O{prU;&8b^SNwbVe7$GT^tFin##9Egsk(ksQ*o(B%XnA3Z6vqV)l=Fa z?7hX=Oxa@T&VyRAEp_4f=jD6HNZSZ3!R@x26?>+A`<+obDmkB@K=`hFkszd44r`rc zUe{{i9O=ibyNjJ6%1jRumyf{d=aQA;e5Gawv#!p&jW7nNC^Ip!L8>ROr+a;m@1}d- zY!0*i&LcQhen~k^(Ph~kFLAkUC#e0%$@G5K0Dzmx*VotW);htd)ynZ9qriVOefE{s z#M~Mm4}izY1K=C6)gnKmq9Sw-cL%@+{0!?T!2GrP8fEOXb|MNm1q}D}9aN0AX;hf3 zpN08BRkm>_jm!S99)LD1Tj@RG)>+=VJ=^TA8U4m2f_+kNxwsIxe7fEP?fW|wf-YqLz@OUgJ7UzOda9%chcs^N89XLetq$2`|%Q2o0*CApR?e`_+RVd(7H59KwsQMyEW@ag42|G&H&)pt)wqstyb>DtybIC z*Q`%G=r(VhW2oOe43xFZmX+Wu_i^nvYDs)jgq_EnzVi#W6*Y-@3+()18qEyEQ5HwZ zii#_Wr+h!Ut$OkBFh2Ak7KY7r7A9!1(`BepD zcqHV238jKo{WKe&&r5hmr?K_vda=Hn&;7X7V!2+M=Kk(&;LWIZ3!9t09J}R8shfT+ znM~TPkRUX9q@1m&H2`{C+)S(f>*Hp>e6%5@+`cFMu4jEQRzu(i1=kT68hWh`E57U_ z@d(^`BE3}p6LYtSiv6T0{(JQY$27?(6Aaq;!33I3*WRB>1k7sP(?mQaG08Y5BejcA zhx)tYH<;XV2`b2n&-13{TTjU+GL@0DSzZ450K{Uo+f|{TqG*TdeiN2bK{OlpsHkG( zo(sF#k~;Pc;j+7A$<}~F=}P>sUyRuT*dE%BRunl$Z029xQOP$K06zwZgN6tua~k*< z*9~TL!VI0?XpKs#Mg@Tl9xpo7gW1UnTT0zKh~?D8u9kt1lw2u4+r8H1&sk5J_H=)Z z9vK78GM#9)++25>>NvC-g1)MhK?Q{GQV0r);%+WDz~`f))2v6yp?D1Z5YNB>mH`VeoOhLA^o}j z#wY8y-kFoICx9CTid2c8DbpM()u=JfTd3oj4w)%Y`H+AGX;eSxufyj)06!)$#s1?f#2_;dPBf{nS9USPX(zVRIB$9Y4d}9nbe`*UnU~a687V9 zSiw3%Kte(r<6>Zt0RYlHu!SGF&G1g~Aq_ZTh#XH=2zmrmvxFVq2>O6Sd0tI>p5WYc zW=YH&5?viFs{I~GrBvKIoUZ|UBj}IxEbH4OAP6ZPM-f5O#RsAP8G~L+9ZU~^!WPre z*JC1FLf7lz!a(w@2UnN75%VhoZ^cp znXuVwuEr+2xX!kTy$FT7vmF8atA?W%ddFsHnMtoHM>|hwb`GnR7|9=6D*oH{`bfmc zgGFxw4QCL#BiVkY1>$m8S(KTRRM9XN!X6-lO%^Lt3bBXdb14DgImm{4-_pRoyw74@ z7gS3|WuNMr>4Ut$6NXRMJ(SP9Ak3)> zfN`sD5uNGtZVOY93XJd^HaSpOF!5-vO2me?QXGded>z21hi8Up3hMFa4u4afv`a0A zb38J~vcjnsotl^D>bea!^@~C-#RXkaqrs{zo<>8dMHkX$=~kM0M#>+02fiK)&})R6iadZt`f8}E}hC$ zS~S!YLn>b48>8eaqs`^~>{t6`URs^Q2Rd_uoGfZTMn+frorA_C#Yd=I5;E5KOYC&( z3i!<_vB_L;b}j$7vd~Dg;A`92@m7e_0p)*=1V7j5Tbpp zR)Sz)lC!&1dKkspSCYh5wzwfdGf|KoMLtdrW+$Ad|TjD{|=;94?;@=P&%8LPi3SO0y!J?gu$-GT7uZ)%|@vAsmma;xbTu z{OzAqR?-zHaU5+&q0GIjPQ;DLq2s|{E9SjS5`JDpzkE|tdL`u1|&QtJTdb|BFphne%e%NC(aEj|H zFeu4!B5i1ENG{_hy`wq6|8XBF2P$MC6bIWx}dTV%@6#Un_yFHevn`j=2 zCWx5jM{8Jt4d$WQpJlrEL8PPSu{=pY)(EldOFWW;CF{V-^Xgm76%8-&V z53|#YjG;sg<%DWwioYcOjJACIOaKMV05X99RWQIj25hUL-&YyNo{A)d)$xkZ&M@E%aiwZxE-t6boRGFYFseHSj;D)%b3CiZFKJ z-O9t6${i7>Q?kYJ+b{BPp88)``6UR2=_c^oFNigj`^Q&&njl6Gz6N)jtym>moL204 zW(NG9*0gLp>x_N)-=q(!v{`<{kv-XUqo%*{Q6XpY5cKw{GQF}B|ZjEx0_J`zNDc(>p~b=t$`D#OGJ`9 zPKUb?pwmZS5^41kCCdTBg!PsGQogp&%^UD?j-bjN}>o`JDpsGJ@U~<0%-Yi!2Z|)eI9- z@Uwp~!jn^;g&qbfG%-R@LsOny`8OXp&(6i63ysSmhK^s(BpiGJSknAU&bur+l~E^ib%4XREM(IY*jQ{)ll@r&!NVJjc{+nJhSk z(hnid1;Z;UO@aymU!)gr=u>l@ocR;U2;LEP)t(OtYE(a30AGPP;fMiihVZjPUYIP> z+jy1+Rp&jqrl=-4D&Phx_=G-fz|Y`=LHYziLX!A( zYFf@!65IK(MWRaM5J8^`!AisgR=>6Oz2a!Jez$?hVn1d-ixE!^OXi!X39X`Tl^9m{$jO>?pv2tE8_0@+3H4;d1FtBYE zT|!U~IHVtIBM=~Voa37V<3y9Xz%c3{m>93W@md!z{3z544GcCh1Y{97f?>Q3pPAF4 z6mdl0*;*60?AW=9YNP3w56V7W!?>VR9DsfA#`Kl59< zS4>7^Bf{(1RY* zK&a;BmdT94MGpufAi_mmv>z@SP7$rfmon4Kn!#;yQBWxrqa0Q*RM`B*jmwc@mp$@n zCwBkt&KwDXac6H280JJNqs?cmZsm}e2;a?P6E_A11_U>lsf7>1EvlDqd^(?=#|bfE z{X;LoePDD{5Lkn~#H2GxXhc$QJUfeQu^o8P#osiw#Zkzkg-KqJ+$>4&Rr6Vu6EF;uh9}i)<|Oo|%60GCcJNh9ZDY1M#wAeRlUFS|+kiyj+_OuqNC-7lo&| zYCXCip?gK8ItqKeY-TJn`|kRRfjNM|t$;}0{)E3auc`+>1;ZqXP%WV%B$#>Qlg<^v zJYS)shXK>a6C)K)gO42F3!UDINpbS^I&L7_w|Z-e^_$@!=Z!pq6Bu~vf<8Xj3_Q$S zp~(>1y(lZDNUYoUDIW1vY>pfmFt`~$_vKwmL-2K3{WP2EwK|AhJ=okzTQ|mxwEj6V zVgFXBU*LK+ISN4-Jg_IcV3Zk$ib4gS`XCgcpt0$gGH_(TK8UmK=wBQ0vMOZLAzTC7 zU}t#p|?}Z`qY%xu)hD2y5{Iu41Ncfa`h#3>3J_U5}78 zq*neV{t{8*3%;SM*iF}0bTJ%8MkP_>Xp}DSJjFzASG%Y8Y=yd_6<~}oMP#z7zt*S& z!gk__Wm>?c@rjIo-1ARZx_vksJd6Fr?90V%GOYfwO4hPt*?J$?abbX7gP(gN_BFb$ z|Gts{MHFO%QKgG$O5eFMNs3M#j6Dm6<>RxEXsLelg(33I+{@|(i>J7}YmL2E(;l1> z6|kwdwAV6tJtXX4Sj=s0vsNka^;|_$Jd=x}E$(@Pl}H#)Pw<9B09f%?OM8%vW5R z=ftlg81VFd9^=UXDlSC1>eTNR_}?PU4hM?>X6N+E@$CB zL<>Y$NRTKe^y0b3ThaH%;1ZA^G)~W(o|6g1o8nt@sC=CizRJ zvcX{7HTBMKeTrb6Y8w@s?qZMI+`$LI?-WKg4Xr*|o^zD%{wPy$<;g1qlLPinAKqY0 zIw$7ve~5%K0yBOjhZ|YJyO(+?trC1DmdAno?kRyc!?mxNc~*6d$kfLv9^9&Kc}^Si z%y0%-3t_Vy}Ny$ z_r2fu-n)K({1$8NwL_B0B$G2U$xh~D2cJOUh!%W|GG|;TB-Xp^T+i5 zJBFGc%2Tw0uKVFt;*o@|1xim{^IiV+8HnokDrK?@E@}s_BRm?Rq9+12WZ%I0zy0m< zZaSAL6s3|+wm)7kL*Xl+~#)yJ9E)kfo)SCLr~^g%a}~1 z>-JH^yJ>s`KgKBr-J=C_bmIW~`D}~b`#=06pg~BFB%oDGl>Op+)~==YQ09yJk<(W# zc*83r_bq5~cUCW~noXzs= z?YH&)xp!LJ(pt5=;;J>ii$f1?ZeO+QaayJLR-7X_+%>h{iMl8g?JYmOPspjH7!}DR zLZJrsya+IN`g4$mQMnc0w1(#Irb&PH~*|ikt}C3B~Yf z;~WjcIp=J>0F|Terwgp``V7h^3h8fxp`6iOy^Qr5^tVcHV0Q)u7M%+ns37V`9td|) z1c?@?zRX|9EJQ<3s@K(P#1m4o;lsNwY)=d#BnfeZX~@kGD)oMZ?tQ4Sm+^j02?y*? zoE{x9bT&r@1k@+-&Rd69Q{IqQ-$TW336_~?<01w1QHv9kJT5H3=yqS&6?Hof`x0ei z#PJ&IqI`fJhGO;?jg}2psecrEz3CS`HwW%2h|=*(z}fRnHduRMMT(F=Yr4Lm3lMmY zLu~By<-O>X)2VZ=ILlu29A#{UC?p}?jrqVn9E7Sm#GtcaI~Ofzc9E2vQdaX>k1jWQ zkZMuw(fde1tDo3++!^>2EdVZ3=meXG44*e;Ay zx#)T)Zb@Sww|B~A~aai<+2(JRi<(zgmr#n@{p)2?dMko?WFAd1v}F z1uibr<_~!?6EgNU;id7;p;i!=`Fci1(ry&4qm~cCby?jT@ZmN~#TB#@7-H9lFT^|k z*ia;WsFgAF?w|Rv60ZBoa_&^fMH15B9C{cE2NWlznR=JLa>sbRLroNZW9o&N&$wx*XJGOGfGhfdtF|%}~GE)<+ymD`ypt-FF{%zG#M#h0RfG z_PL-Uy)SGoP4k+tEqmU_*+cj2pjX1cN&Y(R_e#>Ho%fgP{8t@H=m zf%rT*)pD8HLTC_&-y2jKk*1sb0@4Z#;lkooo&weDqx*`Xj15l|{$vr8ac8OA5^C8U;;i zfu{Cin61tyvN?6WFT@dsd*0e<*YDH>n&-DNXUI(!>+&UItEyY()cdFo94>4&gNL(` zaLYSiev5cY5g&3uduNFHVv{T7BRpD;Ax=bDi*feXD=*VgV@ms4C&i69`Jlp4Iu+`D z&&%OvX({>hX}0i zwnG{FfN!-5RuQw`5eT7`&-X@b(-L--kPR0!ilF)W82|YK9LH28@%hnSV`AO90I4u8 z0egqF3Mm!By0~dCdK428Fn@@3XD_~Md-nIk@D36z$Zm5dleR@Kyv5GB7MfPUHAv85 z4yI}Rk-}Mop~EKM!Hj>Cb+(+*b$FaH=J)>Z_R|tymM}4;c--i z(ZU4HbIWijtxn+!wcN7z-3qWb`l>{D_l=Kkj0g=oRDd;N=Jdi;XB%2RNlSH-%HvT` z74_>QlMSsLyAQ6=AZ?=G>izeN+$0m81?V58Je06*tk}@(t8;=e<`XpS2gZTR0nh2B z2mGZoK#b7TJC}@~a5X$e*Kyrh%*3@Cc9Jf(TplVq{(97NOmRpNX6`+toU>V$q3d=37{Ex4G?w3Ew`vYKGcRo3<<3Huv`6|ZOzWFj^K@(#n zHYfm1_Jxxy>L-J8IF)YVKH(e_sWq=vG=s`+R@*qK8kdk}_#lu2rrC#=+FY zGvmu?NIIeUIeL;;I^5;>G@~yC{iP!e&binJY4v0<5(Mmh+Z)7LmlUC^e4gLo7>q1~ zDg#d=k}=%N-?zK_)~$k{E*3q#ZfDQcj1BsRs9=~0wIG8jnlw-HtSgx{yC067VNn+o zPC}CIHbl|&%8RQw%qjEDHq#e|Yf{~xef!3FU3BIts~~oAQMW#2Rr7h*O%KC+q?FqR z4^#@lWTnQVyDD_2A6tyxx8zaFghDnqPX;C5_c~Jc%^%qrSX$~>NSaL~WpHJXQlMTj z&_!=tasAMg#*9{Z8_fP-*nk@y7^qasNIma6R%C8C+KayyB`$_SOO z)LmU2Z7EE%6}6qnlosX9ojcA|IX!A6;Qc~Y<(=eU5!PL)ucijVX^DD5R}l8Vbj>6u zw^l`**Ow(N=2%@WcE~suzJ#)9YT?oGU@RGRNjm>drZ5vdVzg5LZa^98hug^5#N;Fl zGP+v==4-h(8U#KpxSU5yPQkIT_|nzZ2_hUvBu?MPc1-Qo_e7Z9?E7q7?YnRA@ON%J z&Fgi2!E2{?l z8gBvaOYXYm@r6q?Kl&YoU_PT_2^@)aV0)^|^Uxvam(m{=4&$G`;J5lv`+QWmZ>#>Y z?L7CvqtQ0r=j3ErGYMB6mkt8=m({WjKYXM@-;$_5R1C3{N1C=FdCd=`|K$30=SjWq z7b%eA3{Cq?OAwLVY_Gvl?EbRb8Pq|iLlb68l*|~JAB=1&+o1^2o}p&`0@W|r78Dd3 zTCc9xx)Jzgi*SoNc zRU5(Zln~p_r~9Ux3PX-p%d7AA#v(uCoMZU@)0%Rp;K#b9#BZbKN=C4;flJ+XrK0;OJM5CqbGfeW+FC5H`oJ}(wpHxV z9gUU1LSic@iQ#X#!IC&y+E*y%xHMoy6gG$!NDSY2!mT#p)`ejw|D|E}wwSkCS55ytMu) zEwZLpU>(x?+Y=VOp=C&?8L8(CN6hTKB(kx*s@!3AE6^gKn7{RzxvvE5G%`;#J!l>+ zJM|QUDDsM%64nsBDV;sjn&(lDurquxzd`au=> zApM<|%z8EhA8!gYv-#}??3e2=j(6HUMjxV?cCDnIJJ}-h8UH9JVEz(HK1nbGNyUTOQp0p}>zvNX7+_T8ky=R`RSup~NuuNSpzv(MBwsPFmzF%|OTg?1Hv_@^y=px+o>I3g(9W6 z`aiyP>UM$?6xX(ozFf8lCPj7kA6mSTF=7+CFnxUMJ3J=C3Gv^7l*z=BE~Z2TJY|rV zVkD%fV2}+=mDzu_D=pG)Fk2u3X(&8gyU-Ja3LV0}QDKeA8d!1nxoc&oAOeLVf8WkY z9$~pUt9q*$L`KxMnjI5UJGoYMgqk9D^ce*VnIr1wo8q0~rcX%6O#U(9T+Je(!+8U0U`ZnCZ@|AXiPHCsRuDUl>H6miTHC%uU9KIPI^bXa)YCmhP z7VgtcR$@8vJ|+?6pzlk@+EMFX_JY}H^zq}a(3%KtB++hP#ap|%SI%eqxSfsJJo5Hgb_DVZ>OcuvbxG&N$!jKu6IN+$t$ot zaQzZ7uz}HlO{KV*nVP{rl?3Rx9iZ2`Tr0iPk5$rlYYlQqA zSR>kRTW}Y2bKsbtTNeh$`EM$emd2NSaGIR-?RbP&+SsG;4-J*&UTX*#F@V?|*}k!S z*DM`&i$|cV?rox5NBhDODde--H`mgPs)-6mL*#dUB)Phh3 zpM8GGt1xwxXOMsQs>@k;ImMB7*U_YZbj6H*r_8JI>?|jon3bpIgLmIU<-Scjfpt$~ zvG60W*wcNCU~)C^{TLcB5P^CR(t#q_w%7$pZ&N{ms+ z4Yl9&9Lju+0Cs<@WrwMq|Cc#VZk`DNG+ae@hIU@qj2$^J+l=JbJ8WB*6{o5;R6N(c zd0jF$ybudp2`D&)xXDFx!Wh{j=2&(uoj^6uz%jPHi(Wx)J`;ox=^_T1-q}Us?iX%U zLwURM`_BH4bhC^!>l;4#+ zzK~~49Nu-XT9^a@ie3xU6BRrTHi~?0NW4vrT{&_WlKXy~Z`Zd&O069UUSi6utg-EF zxhe(XfnD3qJ(42g@>7#gXl?Dc?RB&uBV1O}8J^m8risr|iZ+67(!WC(71tN4wL%KJ z-|4n#MC4$Xpu+m80^;PG*@T});%T6TP_bLFngd@(6im9Mdr;(t&^Nb3ASJFs@jg^& zj}@kiHfTwS8mr=CyRU=q?PxPi&_w-CGE}VDdHp5xCzJBK>OETdm*G#^p2%N6=$T%Z z-<=xOy`t(|V!zrVy6rANK3dF(u1tHiX?lb-HOn@0bC%9~b#dJmM;Xo<_+r33*&{J) z;tw%FW(7&sA#4w&tf$~qoeIg*sblg4V2;UhMG&D@Jt7t!a2++`G?XJYnn>NRd0T}u zq!pCxqrMpbFj)%ALa;rY8_DFua-k$2xIJ2tW0|gS&sSE*C$#P*pf6`+oMlHcBov^ku$4ha*N;rv& zUxp8#eifjjt)Xav>0EkMj#noWEppSH8aTuF^=DY82fPMN#qVzA?v&lI>WtN2ajJ-V z^=Qonn(ODA^3t4G8Rr{cJW9tLzQ5ZNE!W3Fc3oCTDhUp;+smIU7UY_o!2>`r;Uqpl z2W1(?w=NKGI`Iu$?WLDIrsnzE#Zw&)QSuoEIChcT)8KUU%J4rEy-~;D5Q|=EuG5BS ztOWy+xA)^zL-ROEut@ki-|d6{35`j{9lTo}SQvO>WoaD6Qci#zAy`*>v!Iev487Bhi zPfgb_I4i}pO2?-%uWkI=%RS4cf zx=JWylyZ3=+(9Htc_2i1f)xKOws@V4fYSS@^`#c<(c`X#MM-Ibihj!~`~;bR^%)#v znT0&GP(>2BRdEMy!m7$Qu=%5KTlXq9#a}8+M3_O^sZZ<<-}pS@6vr1w5Z`L06e+=+ zu5QPD=K>|HI9TQ=tUGs=fjA!G%%0la&6j3%=a+OdpRyeS_f=n^e(x>hdsRU75L+T& zWFzltInJObK466Kl@nI7PjWz#E|`LzJ-`$8e!A-D-UYGWO5E>J%25HUx~->?PzTke z(6xv>g>@K+OK>||nZu4WP-{Df&;84 zQdR=$o*B(;9gLe8auWSBY%Cl9r+c-&mU}^CKOfRiRC4ocv3s})%}R#%1A2q?UbVba z37#iV`E z0JZ(hh&a`Z!t6tw9`4-@#5-C0T8L)5WjL`kYj9E?d#o0fblvgzI9u zC3MQsZ3e&UWb>PYgxfK9p^`ZQxXZy2W!O5=)AL5_i&_!J&nrpAUUHy?{Wo5Lly(nG z9lHzAgr^_;=7p`$gltKoU0ucR0%qHiMk$M1qDxXn!NRg(B8eqn#$@BPPeVEhNq$5m zZYf<1@=+Pjp1M_Sgr)P&`=cbet%Xt^ZE|0vEt|C?NLWU4<4fX8FoQkKzkUxMN*A+; z9FrbKOb_%t{L<}mh&qh)Idn|h&mZZ>n*Ux+UWraMEn$ z%=9b%MufA<@QEcUC8Qk;X;Gijg+>mM4xt=&ib~^=jiU(Y{kUs-M}@&kONMiTuF@qP z-VWAgWoV7oJ82hBl#B-ksFlg9k;q}po`#WZXt2eOaq0;xb#KMYWUxw+hY=SWFOXgA zR*8x9e$nNw!2!Qa1pJ8~UlJja`b%3N8FSXzHrI68KXmBlG7Z+-1Ti!lygvl0c~4;` z?u3nN{?^eBbRnRuvSav%m|7@5TA=&19V_`~bE8LshIIB<=I1XC*=oon&}}FByd%n~pNv`tR4^LjJgXyJ5efNLskIcy9YM z>pr?7@8mTyf@7>{m|tltEKuwZtFiGt)Y1bkATSHgE_AmZb#B7LezgU0;P|a$i`2tU&~5M=wvp_FVuKlJL2zbmm+HEvXU78eJ>5FLXFGM5d{flsR6_>Gkccu3 z6Mp#6PVzN$9OB3^;(nOLIy1Q*6=oc~@b6xFw20%Fh=vHr*emtdY?bgBLo%f~d3e6g z*ic8BiVKTlLQ9CF%Y=kWKu6Wn^D2_=$Vz|{b`s&?B^tLET?MX91Gs}ZLQHNdN-Wr| z%cmlaPBJ{-y0xFIkiE4^j|;o`cw0{J-TLIk4+9QdNPwW27Qoq9uH7cGWnm!5+mvU`(a6HN}qM|1* z8MMM7me;DL-|&-JmYu?fzR`=-Q#E2jp67@|%+%u`6$UcX_hykMdY`&&0^$2&c=ozH z2f77)wce@g=LLV03bW_1 z)ZKOy)7Wj^E~u`MQ?p!Pj3?CP?+KSJ$|kRn)E_#P+cNe;y4HW|mzhY(+>*ytWg^}3ItEo+rhz11|7 zd=t*E9YE+)PWyItW9-m5!KDJJzAUqPRXVU85+ORYEjSe1}81 zEMhP-AxvtI*OiMurZ_V5B#A)5sA%Z@Gp0*zhnRVsE5?yELs!4;4{29%8}IsxOPLr;R=2q>-dT zI3b8c2gzFQ9ey`YL@5hpg@{6s*ja6~R4@F^(_c#i%-W^~(`d`~v!)~0hnPPY%x~^T za*{ikTj0Ls^rje=c_U5asM8;3Y zgtG~TObQaATb;=KP>dK*>NLNWrr^j9Z^RAVFaJEBS&L@AGB}adI`6)UH#l5R1Onz` zc;0$r0U{bQ+AhB$ho2Fva}*xt%M5nCr)L=6R(=iE=g)%S7G39xbZ;z?annHY2~+;x zz9H;=0GI9{vgwb3sem7ISV^jvTnul}g!P34o<+}Xj)HM-)iPoDQcI7y*BYGFatU)_ z!+Yx*8)?g9X$h5BQCUF>W2@3n?+aQsUcH}9%g;Rk2?{k)#@a5qj@QkIr`#j|9W~E| z{ey{(p+V~&9SU*?hio)n)=<1fwGn!%$F`L94|;;l!%+L#RTK7?Z_jrWM@8wgCx%}P zTWasySHDkHwunjeJ(>A6O}qGuq|6UNDpHRU3cYVii!Q= zpFsg2AKrgiEtxcwlTK(x4EjS;S7J}+W}TGr~rkuwofBBmFrQXzS4@uvgoy4O*e4)-J&Qh!m;q__qa9F*U0p>Cxu)p7!_J&Cl!IoPb}B?&<71B0AP2k~ot7 z{)`B1N-nQqs!SkTuya;XDKf`*CHA+(dNC%{8h!tgSfc<*P9+pOE}XKPY#ap;8Y?(J5%2(K&eARo>UE51faJ2M9mT`lJQnCfVDHzLyL=FUstl88nCXxy!$N+@bsUbnJN3xskQN zuZdXlJC+j9G1u(>Gq*S)*=&$|9`d)*md_{MmhEKA=!+J=MGb}x-o|VsVr>fl9SXA& zR8{G~V#;F%b1j?Q5wZ1C_c{Rs;Y_<|}E- zUf2fb6QNrWqd;)fmH!T_Kzo$KVXEdGT|?~7<$UVW`F>VLbI)=vh7`5i=D619Z)`i} zHAQ`+0_0<=4|^A#y-q2W-18GRH$fESJDwE51R{?ft9!zPMtPf8zGY2Kp+kA*ZP(Yn zMbFQg#Ds*2V|!^4IPHFhp__^IdRoxzdFRbnU=E8Hq&)^H{U1)dKOpBrZQ7KSWSKH0 z(I&Kz^~JEke0UBo3(bthrP#9k7S$>a4$!K8#RxneXi>jlrfe3(LZ_AEdi7~-%9{Eu z=iBUd^KX_)Y1E5-4YxfDp^=UB=CQ#cXM-$yT(uF;Vk{r_ex+^~>U}OaH=X@l*RFOk zzW?#k_T`!Niu6S?lwKza8M*$~X@Z59t3}@ZJ%XjSVX-66$xNfm9?$0l6p0wB#U3$l zD$lVjNX1@lMt)~Bhs6jFmdkCQROypPhek;YM;LcPP!|+X-RBxk2T=vLFRQgtI zkMv4TN>TZ+va~_^#sH-A9xn-t(}g>opNjWfDjwG^AS=sPAzUCjNh!fUg5NcRhgx4v*@au|NcWTn$JO{SnZ}bTaOR z7@wC^QKf@;j0(&70}wdx>*T;dy5R_m;D`MZ-y@-->T{dx5-7tW5zwg5EXC9L1KS87 z2dFq-3Ea@ZM3QfF%xvvnLorq&+dcEp-Tb~br!fq!EEe3Xy1rXkw0e_IClVt98X!-3 zX_niW?L``7seRtp@wVao2w}gM+Pr`xy;@*+7|hUWqVngbjrW5-%}?|Kc#oT{$90p!PzvS4N2qk}rXR z%7Y0;W|*SE2H-ESK~Mme1CXZvH%qbvDlbqTK&$`?{y~?(1cl23G-3X$*xwfZr2c07 zXEA_IU@-FEX6K(r@Dl(aiFhzHB>-+CE=0q zp#Cj}g!ebKB=v`j@L&MOG!soZ*_E`P#h@0PPwM=kUqVGZd4*}b#)IiK-;b1wB2L>8 zf?;fY0g}d4QUtwC6pzS=764<4-&j?=*p$w}ToSy1q7A^}4tW|h||ECTlk^EROm<=QJ z+oOmbk8Dh-E>0acA&JQu;P{3jQ^A$IYYHV;t@uHLX0EcrNB@sQOm~~}&-}47+J(=^*BTo`Y{`JT-{;Tf)C`<_Y zC;5LA{wL{-!dwolce()YBc*vpsRG{*wpf_|-F_{xO^RYm-LInSnhGpc7pKH0shodn zL}6P9jC!l1NjOx(EBt)GYW+oXq@)nmVg4&fH7C4epT+w9Gdi&P`{Q6!&-|1+s}kE` zylkRv5IsPB(R*ebDC_#AIWi#6Tn(?NzM%)v@{OvWlatyIRJq0g3@0#L3t@_HXE~&0 zdvni+^$>|i`8Lab)b7{neiw1VXluWvndGWgH`0uYja9W+CA>oM)4Ya}^UTAx~G2?=RTQS?2nZ9)%)B^z&_h`-QkAs_+AjxT;ff#u%)qba% zp@54#C<=kQgYV@xx}t*vuI~oTOH}Ds%s1Lnus)ySe7OHj`1WHyG`*$}qFKo!LY_0k z3((GHGDS3blYrm;;#U&R!FG0GY6HEwh33(72Oaz>Olqq#Bx*_oh=UA-T&nES)v#L9 z`zdM|s#RIpJY}UW_+E=C8PNpmwE(0m6<&xGG_@jYthvFB3o!@nfMwi2ISHGWA)>z+$1>!~a1<(zHLh{j5ESKc*8{76 zBw~ACLjf`$`Pw8l;e7|=#@FV+N8IzNxUSNy?LCpx$Y{3c(V$HVF6hvLWa3(fZIG3$ zJDdI?FLCx9i8XV{($9q9)8`eRE$7Q2%RwJ`k@&b^57?glH2$*cw<@&NoH6nXAK^;- ziwnwYI`KiF+vR>CkzpDUxgFwgui(Ifte?w1N7305P#qUOfw63j^;;1+4g*7}dwjXW zc_?Iskz(Aod1`(2rR@xu0E@KU4m&b+;Uw&oT?oFbxMg~6+;Orktywk^Zc5agM@?<% zqFip@a=1I8StIG)!~#z(ef@hR zfL}T%D;YkBio{k&j#{;zq*rE!|J0M5lPfY$o5@n_Ld8=y#0tVVRsuyxN*WAiXJ==W ze51Scu(Be;p7|0wGuVpL1^iMlLnCQQ*==|2ZoM8Pqj86?FT%8O`~@WKp}c+GO16mk z86!$cb#rV*5LNr_YlaSr9j!!_nXu0 zC7J{H%CA6)K;Z68u2!NF|HyzyI1mtOoC6jQ%mGe-0|9`70my$U0R>_3xM1;M@f7}f z1%QrW@sI#?=Unr*f2m*t)x~>JA>zH_GB5y`G2o;BKL`Rj{}Km4jNw3UU;Sb?`2YL<59c4RN5$v|1!K1=7Ho<#s2uw%xnUsu z!nvHauN9@ZE2R0ppc8~EK0AFvCfo?1&PB=ll#wy}m?GbSi->l}?32S=Z-GC`1lXkj*7AQA$^VDa{r{n% z|4X~yD6nB*gta&R)mEI4W9XnxD4>Nux}7Lo+|#^A%AI0`(#7e1XfD)j+X92rl<|*Q z09^fOV3}dF=yvpUPFXE>j~WOBP%cTTr}muUg+Q`^`(J<+G%Hyy`_Z-57vR%7-~2_K z(O1+vK0Sa&V8>kq+I2F_(TVuiDc?WJIaHBN)yMA@aFZ-#!A{mo7XN_6SH@csc|68O zg&Ke|lla#fJ29$;&e+QW(lZn zMyVnK_fMHvcm z3Aa$y;@fc6tk?!OPJ2MVvPhc|XdZt1=A@aP38GzMn>+CdtnpG?2wr%y0M(GOp!*qx ztshPwO94?B2&i{iWPhDeIGXw)MMjn8Wf4Yabz6$$X~g>xZ*Z;MFEz}2h>1lPOsig9 zT}#a8gK3oz@tlS)L-N2Z=<%eWQaWVe-@k2LRUO{nXt9q zmR>ba93La1BTL3>D10?4g(&wdyLZJK+6h5=IDVvsn)yHwxf$%_g81oK@Acpq%g4T+ zYElZi<`se!_*<}{lNhKNwmcQ`G;&^Gx}@%e1g23Tj6Eb$L41%ptKU7fhgX>1k3U(n z%1S#!dq&2s^~dKuNDj6?q$|^7a!&KF#7$HVF77_Z`0~6A?axX4Dlj1<>u`S}d^+HE zx{N|?17lI0_Z|ar-=B@$lap!OcJy)aw5e$M;WoJTB01QrCX7kfgtQr-Oj6p>B_Pe1i^=D-wLNmz z9XI}QgD{1V@bZ>t7O)wz&(%}=tJGoweoV=q88Wt=obveyp57xom~|)UR#6nHX)0>d zaMHn4%NY>?3Skh#n-S9^?Axiz2<*?SyTXX>X+mKTjN04(#)4eSD}g12g^T7 zgdI}CAXOxmw|Ph}*U3va=ni-D>!+GjJRuKyfsydPzUq#k@;+viivfy9)c`@1&p-8^ z-^?us<(F>qk8)2b^IQXmvs2+a3GsM7V(q_C#Xw=9&`pisDZt&{f#t$MgTs+x)Kr&# z6*Vp%?a_|U3`%pQ(o~OlBSRk4Pz|9(q+h#Uc1qW)T4_F$i(eZXcHez;J9ci^%ihSw z*kgCY#j};}wXdnr%`*6R9F~@4)B(VXU@(Lcgo`JsZblf1EEAFk_~GinBNDm$pN!Ca z$v^p#$ay8~s4^k>U0?`eUJ3A!2?1*V$&gZ~3eJ~Qr^3Yx{gVNZ0bc)<2Sd^gME*ZY z$bPKpsI_1jeilL zqO<6a?(B|@)ezZ!e%RPv_g}KN^FkVuk_x+aP=EJpT z(JvkMt;I{Vc5vvAVrxZAQH1Y2m?^6Qf{qP;79vnlQR#4PYHqHw8q0=e0ii1j&7*3V z4r`x_t!ph$udhR&T=j_FCMVZOh22+{c$;JfknPyY6!QL+ zP{LS9oxZeOI1n{x@6P=FA;_s@EIa&_MYR3d0!up-FePARAlnpboH=39>L7lHikPlh zbWKCDvputSg_bXcB$b_=U1IVzLc&iGJw~sC>&gx9 zgKOY zj0w-YqLFQenOGFx);f_ZdWL&$F!!fqW5Yl|Cl&wjp@tR&qobqq+OPpL2mvG1xU%21 z=Qz?M4^>wki*0O=ULa3~=c|m60)7k1bD^JG+N4Og(=QpyD4V zBXJHKzGgU>U#3Gm2bQwp1cZ^VqaqvVK?A)Fr`)Y;^}?j5vf&JFZ!=HSriG#{&aSgK zZmA*JCE-wLI4);8(e4UG?6J zxLj)FHBY}EaFgqsMUpo>3Cz%xmFKLsoqJzcSh(2?#71(mXt-5gG!n>X4N-MRkHTjO z4botG`h0zHA?J0^_oN9aMeWC`Fj~EE#{2yvG>N}xPSPkRE^$#A5yWbv`$FJqB=&IU zdVYBFp;7l`#&}r2Sfb3?=;*$>Vf<$b62}oA5y!+MGz)cT#yj zPjnW+{?q3{>2*Ece)-oc3DLOrg3hHkL`7yPy`sH53syHoCu~F8zC1Ak;X;n&zCuYO zz>vBAjF?RKHb`RqGYN3#3a{=w-Je}$jz$NF=X#Q`_(auaIc+*;Y>jTKHmqA&hP2(FKB2W1R~k(A00`nt79c5CMrwQadUs@h>X0&I1tJn z9VfU9M`k5jih3(#F%qagzr7d85@1n8gKA3<`*vip^nNQPj#B6euly&fq&{#sW7~sT z%u2iR-p7KBfRBu0$OxSIi(Uy5ODi0}oaBddLO+8spsTc0VJ|MTYJN5hz}Lm!fY>8!K4T3T8G=mDbJ*LtAe$%Tc* z=1=!%ld*%hPFt9GcpUS!1}PSqjkad;&Ex)$5{*HvGi>!BD|q^!nUBIEr<)>vXF$liDRycE%W=~P7hW$ z{6&7k=kPWwX;^ye757SRuog8VGBLX>7z+=}ko`E<)KP{)ofI(=Syq7)VM1QKAOR6E zFth>cGXbq4sW`;Y5sVd6f(R|aYkBcqaN-_~Gzm zwepimZfPAlQ$4yPR`N@pJo3|ecaVaOI_Wpj(cVh5xb5GEhldFX5*NW!p8OHv;T_LU z4_LUkJ(Y4yjEo&XJUj`vmuDY{j*i98M7nkZ@WdAF{SVwU41)rVw@RBs`NkTLjrDN( zd~}a#r=}uIURP$W+B%i0Q(3dUcc)X?FbySLYo2#?Wm7dwHyuRmb90?60h;Y?3w8>y zHoX@w5~!KK9;exg_6oelf7O0>CW2+tq3P6YZ+V+OPN{YF6#2@0gPspA`&7$Iv^0rL zx=A+hi*EwkYvn7RHgb#dR~vPLzE7FKS0;H^e6$|@^HBMneFKr?0g6O-3F|$n2qRM) z-KX}aVCC;T>ko%(_X_L#`{9)sq~eB#!mLK4dtdx}d;4jGgu=p-)I{n^$o&d%Do?Zf7vX(aDMGOtXD(XlP@58?G9NvAgCo%Z8#U0{5DO zM!m6rOu)(-<!!r8FPOa;&fFN3MY-|pOqwQTMLZ3g)-sNg&X~hJR zgizXAd-P3A(rxeVp6%3W>ptR|_!>xW4h^SsfX#Bdl+#zfwM7Rr%YDC%@)%w@#qkzx z4D3m2UmuJ?e$>A@2%iU{XquXtQJEA-9%=!x@`4KsX-h$cudlDJet*i%|AK(BkWweGwsc@OmAO6V`c8|{t!;}3_3ttdERu3`sIGI5R@;}JCEHGQprG1 zzBBjLo8*GUo&YIfIP4nA_hl`AMPA;yUrJLiG zsNi5Y5a`X*`Sta2Vs4jJiaPcGa0V429E~q`W+~p{D!~V z9h)}?;GZP!w&Nt8ps#sJqAU&n@&n!Y%xq}X4=!hCwo~=w@pv$~BCVjHyi(f6>4t90 z*%z@vu^|Fh!*IiI_Fn=AI~_LT&wW6fv9w!v7qYI*RLrrFe|%5Da=+I zk2?T>Cv$IG;m3K`Tl4|dm!5-{1TQ-RA>Z=T`_)?^-S1QeJ5avGC!XaizCKRyIR|*@9wS{*1GEkNE{6+h>1%^>RMkUAx)^wtfiy(GWKnXNwpVp zmlNis@~rp(_+?ahdNcn6r2=M2JX_aR<tg3iSM~=au@sgP+$=Z?I-v3S+vqV9zo&cG;NW0NIywMev4S!xI@%5a#0@{Zy4p4r zfGiKWz7!)G?ee)ub6iPtB7s$h1VNLv3l2*ko7Np&JU{`^`A2@DnE23 z{ysi`E?1uJqnRc%6FNGR?lAYVxmZ9yCbh*~J3&>wfGI|S!8;Gh%t0~d&G`FT|MEtq zGRzc;wmn;rADwyvYSwS21mkfEN>p+s=OWW28o&kgu5A%}FHnn*vm4Vnf75?H)x0~L z9aEk&wl&0dBbgSI{DK`3|0#799l{9jyD?($836=LEHhP$sORZep1Iy!N=>tyFo z0TN5qb>V66vIH5>Uc@JO28pWd#qRfOCWqXbfeawuL) zd#d2&vxOlls(Ej|CC#4YWzLGbw>xN*OK*~Nl@3`fv`pEY>^AhA6|&6d^~7)nYfY7$ z`q~l`pY`&E1d4}vpM5_fQeBNKxXC@w+K!S_0|ClEt}KUA(4@h{_P{{*MXNyd>09a{ z0+U#3M+#*lTH&x!4qBYQA?en&x`Ef|N}|Sarcjzm2fyVYu6~cadXi2ca)r*>>FK^k zuB1tNHaRm^6AKK*=vj}IPyxGBAXjY9&~~mQRtkWDaEgTo`_mMz9v<&~hoyym%tm)u z=b#OLON8F+DG|%CMw*8koQY;k!!fOK_N0$m)iD__@TaF7)9-Py-R2Pi8TYYRT z+{CQphy?9|+v_IS9>;WxD7@x9i@RfZM}2;D`d58~=5PCY3KL{o_)wqEy8ahc-vC`n z*Q_1eb~3T8iEV2#$;8&g$;7s8+qP}nw(aCU^S6b=K+CTUFKd)b8%R>k$|* z7_$t=)$?5&>4liL%^{iAF)y{@Qzk)`$-MH`1y=4Vj@aXYEZ5Tg_UCk|H6W3ZRf(r4 zCKI{CNibWeld&F0tV-)I^bD>c%-nNLGtEC(L|wI=4y&7ozW`S*Cw4|gWj@;roT9Ts z68=tyz->d04!+mpQU6SlaxO^n!mvk1Z|xyTIB+iFw<87idb#4)yf>U5+{1Glk145rsPTJkXCZS2u8P$<(0`vbE^*=4wLVo^+WG ziuXnJimn?q-K(={Q3T|ZvqW__LOGF zb!G&9*KaaAYY-Xu2+nN1l#XPqVn)E^B{$|ipOKxVJKM(<0JxAP&3ozYsbw`^~}Kmaxi1T~z>-+7O|e)cS{plDMT)-E4csmO^9_TL5v!+l^t6WFir zgucfbA;wcPRe-TM%KmN6!9()d4PsiBNK)mnu|^OS6*S=st$6w^ozt2&y@Q-`f%Phc zkx&nYn+Ad!pKdgp5<_~%izsBYz5>!u%3ux_ZL?(X=H6|&!N6N%+RC%&0jQvpIkh#F_mlJ}X&K7AGv%(3Hh09DhB-_BV$q1rEizCq1uebRA z1jJePRdFk<;%IGVq*)BJzMcXI2y$Gm2{eC0Lcqj z1P9hf!Mx;u$-y=!m`k5((5^Da-IXQ#NI{xrvm5=4?|l+BApPCsZP|o=ix9D&(CNn6 zWyRJcPa3PRmI+GzM7W?QsY}1U)ZscU`c^C}X~vt(WRN`zutA9qcCkaWDr-FPsU0Kn z?Yqv2GGKJnxT9Fb^~kd5HBQz2gJ6VU1?_gz0~eYDP1t7E(7e#N%ZyA4CyWX+>s4?n z+I6I@O5=Fe-n)Bw3W=}gS+jiREPXvdN|GFq=%cYN%z0pWyi+q1qhK3~>8qKO?%hhb z&cYTRGoiPyLFoiksta{6e=X}8-m(38OJJlgJ}I8QwODpb)3CGpx~+fm)72>o68%I# zYy8H!5LaOK-tw}f8841XK38k<`FDi0HQ-Q2UEAAdASEW;Z3oPd z&3Gd6CAE~CKWYAk#?LsLa(eKq@^lX0Wv?rd~w^iKoOm;Uv z9lwdv<3`yx{~W)ZnguY=z=moUPYd(G+@{J*0JDLjLr`yQnNExpt&HvSDgQtVa8Y+_ z68D9HzaHe&G#I0L0g|^;L>v5F$_B|pTYH--Is+4`Q5Jb{)?y8a19MxDV zqZqFk3Q=|nSNzvmP9AeDu1@s*Dfovy3LB}78r69wf+H6^1OhMytTz+n{ct1dsd%!u z)o7AC)bdQhMd?(;*iDH<6r+D&8F}Z*D3sQUDvDA4_1m+nCLma=ezK|;6bF>8_+5Qi3@#34W$p1Acfu)-=l(}&bnoKT1Pz?DM8Wx zsUFHKwXo2k`}fJ@CvI6egYX~iw=)cSog!ixhw#wXS^`LFgLO-!`wi*>zs&5bge!^} zCctCOh<>s==$p8x5o|=as!v4TbZC(MJH&n7kcbNk92c>IcV+ z6>J2@j0g-JGRm`#oPHhh>SeMae{}?Z`F#^oFTL)^)>qq8X5HlG5$%WE+0mwjwegdt zl+mxa)p)NWhW^cTRfl$LqMQE6awFNvnXCHJKaxJ7F3v2R^yyGr5`7_yp*EP7oGsyg z=s&b|bJ+2C2m9W!Sw<+>K$I^`=XZS`d#VXmeLWa85*1kfYQy12M(a=AFiiC6F9f07 ziX6@l_I)M>wg|Iu_pB@ZMPvtM$5BEK=rBPx)#n!n{P)-G`pWJ$4FNg@URKPO`)wllFOe0ZWaV-&AU% zEMAR^*_Pnt@k=u%<8|z$!x>k!c0v91TR4?Zu*0m&%CH2lR}jMlif{G0{2ZmVP)Vg> zF+WrNt6Hw^+p3unSByr~vkt@W*ubGiw<`Ei;%WqxvTuL<6sJYBWnzro4b7u+Q&iIm zJlL7!v@zx$HJ93UbZ_k0H~m=+OOV7%_R?RN@Pzpn@D6hPq!^T8@0s_|UxLw8#uc1) zJO7Aw0eE`A&`g#D2w0#>L+uqlF$O#cdq;3eV*$%4#Dxi{{91XyL-!M0Yv|#dir5E$ zsY#BooM1o4-1<(Mw?bTmy`Jj^^On(D%AFLFI_i+VP7tXZw2*K=~>O;LF_nA zzC5Iy6VQrz$lXx*^aA5D81i^(xCUX<^t-yN$XtRa5e~8k2Ci9a6gbS4Zy5m&?h~rM zE9I!f-dC~H>Vc8Y;nh!bTVq=tc+O-mLqO9GiTE=7`Fn<5pnzJ#Yc+}36AzLWKS$!h zN29`bcf7vsff^xN+|ifkH;+mIw1Nez!aJ)t!=h?T-vB7}6@v^L=R;Uz)k=zAD;Ho( zs#d=t;-~MM*PlXP?7!wjiL?krIa>IBEw4k_tc3r9R#H1dV5zA#m-}p=(4tVh&zato zvMpMLAPP&Dve~Sz9Q?e?$W`cc?-rB$vdkj3n+z-99q2 z+h;$cxG4D_`QrCZd5nnWZ4rEF7Gk3AR6@_P2vuPGI5}s+R*;INCWR_WeTaJ^wne-6 z0iw5x%lF7Z`{{!b*47<%p7gI zF}NOKW;FPVl>-aO&5e-Km7FDh@UQPDLZtNbXnH>;Va_LihFGOYBP&%yw$**u5t|%6(e>2lvqLeLO*52Ie&Y>bw|GlVVeni z2|K}*F&w!6JN>F|ZX~Mo;^Wq?C-7L_xs4k@Hg_r!_u47i@lMLAkx2iLQ#O00-SK zWF{H$f(()PUztK-Rb1_FGrxQLg||(iy>P0q1+Z3xZAPq{WHgG2s=5%2`hqH?yC9yn zj!kDO#|wY%@jvXIvk8lgp>nJ@2Z&lQr^)gRekbd_+yh&6(SCb5rGQRut$raLu&_#O zr2A(cT-_WBukw4d)yG?heZAv}41g0t+!JT@R1+DXSeU-psgDC5lS7;73^hy}vepgN z-tDr@E^mSR0!H3O{C@~j*4*vTU$HR)$G7ie9_Q|!X`jWKanm2!bhqnbA(8(gl=`m$ zaO!-_iq~w)V{JTfg`D=zTvy>YY%6FNxFgIDj$!$zqsTE+GR6%SOo-K<)!L{(0s*w& zgd?Hv{-7WO%Wivc_9E;v& zi1ik3uHk=QWYVDcDnYyD6OM{~Ytn-ARo_Mhez zeM5iqCjfQz{Jo~?eWj~~+;J=rU^$oRo+miq1;|SFpN7E|zQNM1o6r{9ES*E9#0v%( zZmz(=K2EaZDDgB~jB}+)5eWPfJ8aQ8ilez4Y6T9R8x^pCDfiQCr!i`a8fwrPiho%& z2Sg|r#L*69xb^lLAs$zwRk;`BFS{Kk!!$IyRI1tp%58>lrVTcnh z+5KVe!%acsBGl1>ZhTmE{9u0(hqjU!6g^gE5w?q<&Mi9z{q~zqN_l`sZbq`ug11>F zm9Hf9OLUcg_@g_cu@}_8<`jjZHhkciuP3e9W{E|70nyF2%Q_!iId&>LLIxBzBnpV7 zIx-$nux;$J0eP+2g{e znYhIH^s8Zwekdr}Cj8hJ7L!ashQ|8OK*7;#bW_tf?5p%;| z56=A@7wd6_3Adx|X+LS&5yO}Ev|RNv*6!}{Cc?{)BV!O>qyV?dZ76GU>$PQ^p?~B| zOOVwPZWVC@GlH4wjr*=F8VU<~@_uSCB1Xmm`U|O12_-N9{7}p>me_;O4$3%?|J363 z)Sv@0%FuJXdQ7LPX?PK6GAFURuyctW0Wm5t5lNwlajzP@+>mq6%>7kB#ab2kQdLSo zw8KZFX90uS1TGkV5$2x*wmBHyR7ro+fLf+Vl5NZb;4B*s?UE}Y*mz@M8uDMs+o^pZ;j}(nqOd61L5}O>Y+Z$TxIu_Nzb+-U z#L})?Aa|#e?FOK|f9gO3-R`SDv~h()|7bG!b=c$f!mNM^j{#eC!4)V<#3WS2_B3VG zCjTtY%m7H$>SG`D*EflEz~=8Ohj4CjPhy~i&{6Y0bT`Q9Oc05ZS|EA?M5n~sgbeVALJs6WhA{Ac`?&&pU<~ z+V)r@J)ySBjjxXvvdn)r*TX?^^>Ug57Pkb5d0!A^p8pxM4@?-%)tk4NiNBbQX?&2F zTdGIY!GfSkQqkvAJGvb9;3~0;_FAVvVU*2(77tO zG|F3x_*3llnGx<~NJa+p#DI(egTlYg#Z#v3EcQC$roY-*A>u6K>fu`1xCYTxd9Ip; zp=W92Xd?QBu>RwLImWggl(zWywPb)!A_4jZbmDNT>w81;3S9~G{CV~7@q=ub)B|8f>VfTZr`wFp%3{OSc zt7!J^A+9yYO&#M!Nd|^7+x~Yz7^6{d5=;}Gymle%D|gN2^iMpkKT4o>;zX!$MXv2i zR^WF%{WSeyiW(>344_+xPjr?5k@`XN%07QNvSq1x6Bdv-Vl+YC)5{AH=oV zZL;?Fm@mFNoNd-gcA@FGLgesKVW7q7C0>wsT79KkeI1SLj_#IraB$OCc7d&SHb$$>j={Er=AB9*X5PuaRcwT6K z3|C0pfkztRU~q-$ht0LG&&SZZW?EMMqjk5R-Ihz`>Gqe7kLSEBNmpqdCXtV2k7bfw zLQ~`-Eri^;vsG8z7$RgeELpCdn^VMWOvIZQWNWt|qFedLZC z^W>f%Z6B*#xF?>901}>&>@H2br@7zNkWgoEwTo%8PsW>-f$e0Gtn6_fbxTbP{<9u6c&p3Qjep7vWK)xyC zZ?IlEnOT+T^6bM1)vS-tHJ&K4TDz0%k1ft#P7&CM_o~03g`r$l$W;HL{;yO~-a3Rj zxk?qGBokqhh3I1(N@A|`6PXF!Q=?nbDhf7MO-_JjQ+{B5l(?^;JkCOEO+#-qE{l(x2X%765 z_llR!Jf0nxrMsuM2VPkXrz!19m3rQ}P0W|{dG}7T7&^k;fGe%m>Wc)gBD^oy8!%h7 zU0TTCaaes%+jYI6_=ufAY#uLoy;qwqOuesH#TbBWw)j3u%=QY^aNDY$j%6>_)`rA@ zw%F;`qPuj!o(v#MAAZTUJ=eQ7NX2ro@tPt$oUuf&*?M&}B-i(Ri9P7@)f=?EKHz6# z&}ekbCTd@-!}|{y^Lbotr#} zLKUXKVqB-`+US)1efen!-{N@2ak^pxh0oftXl^z&rL+&OqTdTT%&ZWwZfg3G8 zJc+j2V$bkb=?{qEhSk;`w!D9qgV1=LkynIooiB4-YNRKHlKfAdX2Fda|LS?%1^ye&>2BR1Hz(7H4 z(+{ZI_JqjO_SF|eh_y5hLZzv6YHl2Im`E~w)2~k0{fqDTGk7Ho-}vI34zps;qzmVL}Bs9cp|A;G-iaS5f?tt@UmtL>b%v+|5w z%#p)yn=MTDKVP-F_N(~lDM!CrE5tkEvYW_BTCM#80vjj=m@wuvaM5swsMY1E}+PwLsB*Rfp! zSh7>>vpJE2s{SVc=3r5`Btgm9o;fPs&VZaM+i#$3#f!r z*Lgwvy9PpCI?khXT;L{D4HokmZTRKtj%c&Dqf;E>tbA!D)h8b}ay83o_-w%iBk_0z zHF1L0UPQhE{3i|G1=}4k4AwanTmDEM{@EYL zdIiuMpS+d*`0g7o9bI2#ZUlq;lQ^r!enlUDiw~0-WNjp>yeoY*ZT|US2W3w-%MkS)Q0FZZ|d` zhwNml!>JVRiwOIc8O!j2u(h`g#Ger~UT&X}F))B@>8=2ssp8jmR|%3vS$b z51)^Y2v?^oS)-S)zSEcLZ+kK*wG;b&PLrWh+0c6%A|^K5mZzH)h;$msFm}}JQF=C- z?xPVd9aSBxakENIB~P#^R79R}T7@&GjHJLj*CZY3;n zxTqolABp7l9kzXetmgZqsRVekkum~%Nrp<7k#RDqYDz(m8DQM{@XM)diRtj^J9BP+)o=!shmCd{;6wwwCq(7fMEd}J%Ug9d(h+YVw zi(^Su(-c{mMc-$__DR#I@K&BLN1y7AFT|vzr^w@avmL=jMFy{_ZJ7t?q)>>`YN=6A z$c4h!dESBZv5_WX4aB+(883FwgRih5u5;ijdwY*0U?w_0#`pJ7s7zadT$jNa!&ERu zTuJHrr})F^Q{>%%k@`lP>v>5X8bKC`pgFL%=|bcKfAbg!CHGOu@T2q(I@NYML{r>w z_s?--dC&3aEqew3*j@_$Jv^JQ${g6%UFv?i*n3)3YyuUx(RylVJ({(*8ry!K zi{Fbw(lGYTmfQ;w(gJ9ogiB8a<1W2DlF9VO5_=FPI2tLD#bk1(be=XG-{rU1q)ziN zm3oos-e85pb~p0zAatM%DZNdXaqbmT44NGwn+1V~+Y2}Od)BSKIEtVQ3OdvqV??9n zm~aLb|7=o^On2wYBXf(~O@YOdXZID7L>M_WqIT#b^E`mM(wl_UkN~uej|di|_Vv*P z5-dPa$64FwmtY9AMX`PhN@1{+u^69`(cw+0abmfp)7h$Y_pr*9f;L?RnLv)&-XI*& z<4B$0l^2nf>5%R;rMI{i<%LF*ffAA&O)LjUb+=PZ8QfO}c1twy2nNnnK9m&}^B*)~ zDe~D^b0!<7A#MzyMBvJ7dG80iMV*FRP`{@K*6c5$& z9$}1jTQjVdM@gMh@Q14MYm)@Z zD=20w$$zhg2j6EOKvewkc(S(7H3GRWHl8$yevH!tBJ81Qbb&jc zovXucr+`wp*^_`IwSV9$UzZw{zP(jxysHNiuoyl+4%pwpCMv5Z8Im!cw#RoYLtmRS znVcPhlR%aQCQ{Nj;1-Mr>IC)eZJCN<43MiL8Z=DNEfC_JXVgj zV@A>s0s1(=J5rrOM6Grv`RSxt>y{R9A-kVQ=mz}dBzdth4G4QW8ziH^yKP<9NDO%6 zIc?x!7y*z7X=K#Jpe30+;=bRN?2-2{6@Q`c02l+~MR1(2HW?bp$SS~o*5D}i^g<6# zm9N|_SWoAV@Da%bf1b`W#1**A8LKO*h=A+DmR)i;W_5=$e(qpIV>c0jZ}muVs=dej zHek+G>UN_dSLlO)FENWVE>3eK2-2o%bIQ!?k&&AwcMk+%ocXGFk_CG105OhBtSqox z{4&l0mH-yqs79n|0@AOJ^4T=>*74Sziz!W@iGFm1`$TI?Ww-PmMppYItLR@C2;xgW z9zjkNArGR?)wAHX&2J#4RBxEl?uZ-3uExBI0V?TF_MQ@P-qpyzqss7b?V@{aWyywI z5y1^3qdt7lKi7yTzV|c$db?mG_Qc8yE*C)jnkcH|u@+DR%G`2s!xDGe1-?$kH=x9RtaN5W+kzxDRRgq&JT&1Sx-^*$oU!n+_s5T^md{?`Y7r%QIX8 zL}|nytj!HV>MP*Z8K@l|37y3i07i_DcTSgq@Vh2k-q=FtAAZDwj7D)CLC#1H#JRrk z@4>f>Kf@_VeCNfNsQB&yQ_pP-1OK;qR0FBa46vSWXv$n?W0U%#to!=psc%6eXmF-^|a#i!kxZB=IP=}AYLUJh zB@iW?`7#=!*0VC+V$Y0KyNz;5WnrLBAu~u(S0Di(84Xq|;O_Bu%`9f7qxrS1@6T>jfLJ|f7Gwob^Y*d*1UlSPy-o?q)EHo^H zOTmUdBDT0HUOW;zp6Ni$-mRoc%=*imHMW!IB0L5ekW7JKrcNvZDl*65E!8lJJ~}+> zAVyareXg}pfRzJaNbIgs90xa`w`iWS!B&-Ss6ooIE|#$@sjZqN`s@cZbD@q#E8G$^lBT zD6j3x-$NQF(~@XQ+~?Z+lI!D{WmT$=z|BmSLD9DPm7$K{9ywNl+>j&s*xdEWi6L`+ zx3Y5l#F=`ihk_tvV!tcb)JHp!H-Rc}3=fjbT$b@-P;BXSGvMI?6@3~^uK;q5B{-bgHubHe zxY%&>hJyiuR%=_x1lc5o>n>1=J`@?!b4&$RWVeKHsgn{hdN_T6JMOPC&tqJzi4MV$ zz;Yn2fT*=oEetJQ?vu;E1HRWjR7qvp;~g&c<{9j#73&D~{rF=?z?tBUkIoA!Ao@8oGwJ?<%ofIEIH&RnZK!1Dk$P^mK73n zKhq^2pASrdMr#Cja6TivTbQ$W=1UZ>{VtTX?F<3F z2Nw1X>AVM<_cu&S>q{xx5}l^q!4ngY9X;`MYB7&)55)E)z7BZyfgOw+nJ$Yr!DZNE zKAs*Da(Mi=7t3=~OdvUBk%N`k+LX*dM0`k~VgYuw+!~FDM}OPbpLwtFKoBvIfCo#s zqI-X=)$@qfv3MjT5L~SB!k8d&q}g;d-9uH|V^J=|rX5P~XHmP#uJ5@@BQ>N&P)0b#KfDF){mha(h=)700 zLivgDuG>O?-Hh_{b5HE+m5FFm$9vdl)~tT1Z8-z2*YrlU4DVawiDQ-CKCk4eeWbDf z22+Um>}N@7s(LR=(9_H++vz{aVlwTTr=ZHoXw~D#4#r$1`Lvp=WqTYSc0Yk8Hcc-6 z!%USj>9m7{xROL%qQlS-&1*gzcuxDxQwgKL5)7FIXP_q?Lh{F_@Rzq~?-p-smmv4u z)aYHm|H5?82lKJc=O+C*>MU}?Td>rZz9O#7x37b*b4nmRptUDn`h`VbbOSf&Tz2)D z0hk&+CTj{PFactj|< zxXUSn@iwd@H8$g{z+({HU=fg-rK?SImAk?*9b*wN!<56N)*s4i_gGF@(u@5qP^pkk z4e=rPFs6WcuG@_lW~ZY}Fsx`uPB0zePSM)tb>vQZW6GMiGr_~AC z>AH{|huZH&?mv{fYgYRiPPEWR6RM}7W03}sdk$q&p?oCNV+Y)*q;O`+hBWHhZy6)7($79}h=P)BqH6YIkDX~W`MCY}E9d$rNW&TYfkdorV! zX%Bm?%zgdXnOb-k;hva~R#kz)KJV@@pkiYv$?w}>=dTJ0O$=fHN|eZQS@w;KWM*E1 zMw-*-{cd;9DJnK_YkWsr8{~bjZ=Hhi(JQXyAPBtNAL`iE2|7_2lPqV_+s2HfWwu^D zgSy4AcuynuA+*nwI`=}I$Y5sR!M5K|-8AC4JayCmwxv6Br8tu-3V0=)T6~-A{H9S9CxG8T~H>tA-1ZiN_9Z0bn5|L^pFxKsXK9Z>-Ow-AtSvBdwWpQV45 z0i=K)^stEvRH{Fj0{Cyz!UD7opj-c+qx)~&e-9=Vpl7;4ENH;0tYr-v;Ot>SbjevQ z+vPb85P_JS+@y{>pt9ZL-s_LK`DCm%l{QKYg>ymhnp`CjaCGT;O8o&G&2Y9EEa`I` zT&MF6XCyRQb%mOzWw(y_tE?*?`Wp~(EclStpd%hPXD8#w%N4z+c6;N)Q-GdYkNKaA z%PLz>OUpjT?*CC;>v8t&;=<}I&WfFS&-nVOpI6OUDWJ&b)50ksP(m|Ba7Qa_4dxQb zXU}(qUBXPk^8(r6Hd}#<{!*SPWM`u{!Z)TUWH&qJaO6xz0#l!6P4Vo1j5M|u(}ozn zOyG*G>Bq3sn;8wi;*LF)IpmAtaY(>^)f#M2bhT5pe>~#${4p5k1;|+gK~qkk zjJ;N*BDzFA5OYW){k%}uwETHh*9U)ib30i8m7J40yVt!rC0ioH9CepHm$`oaPXS1ZxxAJ{J!V?;Bm*{tZ6w=Qk6iuj5kR6wzQWPeLYY-p5`ch21Re09$9Wh7Ra|Z{AIr-94Xg zd&D(%Yv`6+ZlD{Ut5w~k2=QAOVtrRZP&|JO`zyHRcjj2|j_COl>rsygcBc&AGA%gW zc05b{LU%}D@pMiDKkiO-CEjkE8yY&fg4i;z%z_Cfq<80Gb4~ zg-Q))PR^9LSQ1VM z@t631&06-jrV9p3QhIMNmH4#DxL*Bv7vaYsLC?0NqUkyf$P)q2+mb+m(1?3;Q=X4+ zj2#r-ad6?~WAbtl2z}*)$`P}0mG!x;LkzdOIa+_BBwMw8D$VHk}IcYT7Ldg>Q`I&dBAK7K1kszq{Xs!f<$r!-E6~7dNV~gXCHTKdp10 zwsPYS*eE;rw>=Y4z43bz^`NBL;H*JWTRoN6UXUuOi;V+*TAMI3 zr^K!)ke*w|0oC0aob}ZRa9QfvCM<5hG>!@;)9?fCjzm7!7i}ukv78$h#P!nFp&!&W z)O%J((UbtN9f~p#_78ZRrk1bf`FCjbc52n=TaRZN{|3M#HC_2Ubc9^j1-|=RoVcbx9mg+O7&zF= zRWu=^VARpT|DeG9isbQ~_HV3hJ&tW(_cHAdH#&+@C*Y3|P5VUVMDrjlqUY&P2}9Hi z4-tM3D|QOIdXikR;2_8|`T}i!B7&fM_1_yW5XWMV$-afEu@!jpK@R>_j7MC&{<$PT zru>-R+&Ka(?dc>)p#zzc7h9zZ%mZ%~%q16qRz2TWEmwfeG&2fSU;fzuh|sp;o9{2x z>Ioe!1q3jMUnIRsb2UJNoKauog_QSL)wH4hU2AwVDYXj%x$5JE8^(3x@#4N)tNJ)%nDo z|J{J)VrL|}fv~2lTwN`q1+%)^!wf5E6lPAi@(ExzaG}IRb~BFgjamtAa|VfTPD2j< z&Ie)p!*3xHS(20{H@L;^uG$tnBqZ zI59;gt0H*o%)93XFpQ1W2*HXCSqy~>T8(D()j^-59z@S#;CB`ZAj)gi?Iz_$(;Z-8 z^FQ_t^56DRXQuc+&=xUDtZAg}?URp&W6Y1nGW&1}F6>E6O_?k+wgnDM3UAbH&^myCJYV^9fBBj!6M2Xoa z_Bmctsu%r`v#$I_RZUXGQX$DLlO?vI44LVLuX4K<9~ie}s$#_|eB#jjM_$Co*XSTw zl^vA28D4PHT@)?5P+as$uXx4*VYJ!7+eobviXG+U7TvcY1z65eoJxU7yk-_&pgz^dF4^7uNy^)5+2uksI?~Rp-6mg z5$DVG5DNKBqJ{&&=F?_}6BH$B0&GHEsePdL)+&m@^D{w!>?1Ytqlt8y{l5Xv!o71v z@;A<;#Ka&`g#2Q5wQf(p54Hz7)ObtiY=12EV5uPv^1>&$(md>_VYEw>05&}bmz#G!}$#Az{LgjifhG{UM4ShtJ6tnZ(~5Lz#v8G=jBFg8NvBF<5=QF z0#YomG=`A&uB(!01&dd}X$NHPU5xZGn#(65uLE!dND^^S&vlX`l2WVVMQKA1m|LQq zjl{$k)oMiJyrAe;&(IU0$};ZA%?{s2Z?)}_(a(UR?itN%+-gL&ci$RQtG#TODce@bIclXNYT#>P)7hQ|(@Ejo`2t z1t8HsAPJSTuP}?JZ`i&@;SRCC6vDy}ZEWZRJ;&Bip1XW{d|$?O_?z0v_TLN^s z1L0OXK~P}hn6bOwQG|m&g8`qkF!=c3XFA-R4L?t5;&@!_4wzS#I)9836D;1QaS@s@ zs`~YS?664hEQX|-V3QZur&BuRxual4Yu|h$tPxw@jnOaIf z7ghLjyU(;T70a}@rCzv*muU70IaW{jk#oWq*ipvKb;&KuDVeJksTFW^T;h*@$e~yU zmzB26B+;RHIHg0x{;R>YVYx0;8l3=jqD(hpZ9m@XzU=-4wF!4MHA`6qQgB)k`1NvH zfi7aZ*R7zzDDJL9MGX=iU*&TRL1g+>8judb&X<0}0b7_{T$0u3Fm{go#d_1z*A-aL zbb$d6c2V2V6LkaGlLWe0CWU!`^x}G_@3e@N)7KdRCPi70SIDX@4H2s$44*6JL zUq`~mh90dk7*c33R|O8I`BAC6KOEC+v*T+C;MApD>ilz~uh8mvM&9`PdRBEaC(n;e z3{JbTv4KoLK<}tlu8wRl7#?G<)BZ>CT%$^xH&b{!ZnoY`*%L9*VE^>$gHTH&e0fY- zXZC>F{5DQRS$_&F_0Y>zxhD)U{B4!!Q6K3p&2&9NOG3_kzSsw_+1u=z_@Y9&NG`jY zx4OgS&!1qAw^Hpv2zZ?B$BQ*iw@2E+NPHTn%h>{0N-+FQ(8e@?JNpPB>9xGv9l>I` zxVp}=*N4=bP*70R=p9XF{M7Co%a@D;%O8p&i~_6~+5sfLN#?L7&{^c6xT-;*<|=NV z%T_86r(0iB3DP_>ht_CQRjq$1$O+AqU?ux`fj*Bx@X40EB~r#=>)X(BAf1iUeU!}k z3lQ5zLq1@vI;|qMqc>*f0Nm@smMg9Bhbwg@UkewUp2Rq)^L30}cAib89}5A5}j(%NI3 ztC=YdMc%zo5G}Uz9;lQ z?vky+if2QLz_{GX_@lXTl-n-*U*)K}jbF>H>Cv!yuOzZ6~EMq9Y_m|IC>aZn|bv0V-$P#IP z_R{49DuONMbtRuc;h(IOl<&E@xkxxT zp`IVFJC0K`}P%@l_lYT#5t> zq)^4-R@Tw|IGmVfe9pK~CA3TYlLg1qB;4pt{qV9#$H8~*q9+?2#H=>k!D-N-rgOOM zSYaG_&!i~Ej}`M~3T1I0+6jG>5Xkzm9jcp(^60J#$U8eZZ5U&P2ak_$Ub^KVxi0=6 zWp5o-SM05e;_eQ`DHJyDR@{nj%0`O2yB2rX;zf#=jk^_hw?gsa?!}$AX@BS3d+vSj zk2eNmk<84@By(paUy`-f=W?%^>cWObA)FcCbKhrJQTZ(wa_lZTaoTQdB11Y&%xPw{ z-c32bgfFp`+r#t@>0z-U<)8ne;wDM4VPizgsSM0Lk-C3U2R zW!7>-6BmJiRZsj~`t=&8EdO+A-<@meW~kb5_4CzSHGQ$bvl4v^zn~z4mkfi~z2RVd zu4bcM8(*KWurS=4=Ife5xd;gzXbI5h z;$qxey!OLRA#j~}TKLI&{0P+eslm9OtTIOwBufDA5a$Ox<67wB31-SAmv`&kY3 z^?i^jWLvBP*{cHbAtW$)q=aZLZc$e_0WrVx4jP%%Y%Vlnm?C>g=V_Orc`&Y{5<|z+ z^_v$l+8>aSSr#~dAv(ZWSaY}m2-aOW3)5Q&AKMLNVDGx+)&Ceg{}Desiemqv?&64_CRk7vof|~)IN_pLnE~Q ztE{Ay*ps)(XGjY*iNlWN0qWNaT+%Zymy&y?2iIA&<)E@ehI=QQj%0 zE0}9vThEpBJ$ftuX=bz}KRG!HNfF4yBfY!7N6B>A|EVTwx;3c6RGB%Weoi(E9z1@8 zN9B_F<~r!O+TYIL?DX@Kq2zAfZL!1`xF;1Usqa^{FQwrq7i^~^rk@B8xg5*lO3CjC z4oFIik$l(18*ViVvElY~tSNh}zyGvzak^oQpA;24yt=uzqf~N|8i0J;$sa?I%7s%vh>1J&ZE^EU0jhMRl{76BLO7IE`IyzKND-HHjUL71D6L{wYB!=z;hg zbe(UuWWc23%+lu!l8z%E)AI@p+Ubqh{9S9V98m|H-|REgbxFTtd)IpgFFKfmD-#ws zg*nzW9r_asKxCU!^)7n~2PMn$TVmUG^x)+BOsJCMN4}6Yog*Q0As0zS(QS0o9qeE7 zIOP7WMZ4<W9O(Lnd>n*$>7A?O{2y z!%EBQ=vs5+6$YSCq(rAXd@M0@hNdXL$>wla6J<^)3?Q56doY9ncn`HQNBFsgNwg;2 zQ=i;qh5LObaCDEOqN|~aZuyPnC5*&C2#vp@50inWmVc&21X)xpNYh8NVJ~w5shDcs zslg&QSYi+xUq*f^I&CPF=6gouR z8Y=Z%lh3a^2${&{Wu^JUfoFM&I$*m{vwglYmB&mQxe&oQm*eY`zpxb`&Pg> zQKSQnRjbyF*)R;M$H|4f@J5^TquT7!l8k4oVBINY4u{!jB}eQ20EM4|!?^V<7{hVv zkatovWFj(YLgj85C6rYmS>+3Ct$c@7EJ_@^@=vGicUk$}hpsAhb;3%MiZRH+ zr64~O4%BYBqI11;f5#pJ?6rv9`HTuaT5gUS(DRCo>>EJP^gd7Uwet5l{;}8eqEHjx zZ4ous^mWkAz*n_ifwkvGkN>hX-&4j$^*GKF?h_2Nm}z%#M7q#C12`ZOtyH_kEsJys ze9nH~eZ9Fmmty(z#0F*SUHFF<>t6@j4WIaVOou_zFGzD*UW92Zsz11Bc*>MTO)t!# z@rhv?9H-5Ecf4c+)NnBhr~m#|^6ECtqf`bp2dVCCOaL58bkM3rvk$y_3H#?bP+~mPR&i6$_BnJ5? zLdNN{tT4V%7o1Ui(Hr^t*S^GMaQb9MneU=I;#H4ZGpSwWmZ=bGonlT%->z7#rS@8yCptaY#kt z8It6)YZU}TlEw&eX=sA+?1+U7wYMgv0+E&kv+!54Q)vJsq~|SDQ`8QJuT%3w^MQt& z{AV|C05pmm4(6Rv9c>Ghm)wCNUg`yff@ip(%iiD~N*I>oJWlFLA*ywd3Ium7t7fKe4i_vZl9_+il14|jspZIpg&nwib^7&5)%=r0&qn&}b zC)+|BlN|g!=HohWh+%BhqVF=u3^GNAEKm=rkSwpT65v2?$0)wm*ieT|UxBr&KseI2 z1dG8rsoQZ~=vusNnWLdY^S2SD9gchiO9k`8xlKYmwky;*d0k)q<)Fk&sRUe46f9;I z=`XNYxCD_jEo_SeYU*{T^57)Kq@7roty-#?YZRR@qUQNOLSvu8hw+|2mPrIz`0{o) z)+Qzj%e)Rgs*coZek)0H1ib+E{ zbfK{B$UoP*o;j)XHm{EBDiGT7aum`@_{v!P$N5y)rzs~WB}`smQlY&!;*92b3pFOQ2a1|PWkBc*!W;c;h037e7Up1FD?o%Os@L5r=2F}3LuyFd z^J&1v8`kv2?(pSYiLjig*DZ@jiISYR?iHCX(W-};I$2RY)+=G#pYSDtFTYPkft!(_ zJLhdLbHBa#$pmfwKh4PRitiuZrC98)DoG{$aNhob`8@E(6*wjK?_BkDkt=Ls8do>V z`O>wt!3-vdxLZ3ubLUQs9QFCKQ;WHR@fX$kT_5_xK|w)@PQJ{Mvs(l&5dd<}UvhB( zuK^su3k4vz#`Zsff5QJxJ%7PW1}-!Jk(2*B0K9vFL~T)%|5xCDOZ}%7A_so-0|yT` z{3jp>w$X+}U&8(c{*TnZYX2+s|4R+!m7Tie!GCkfwHI3G8b8G0%;WnNxj#V{@JV7t%5u{}jK z5nV%8m$AT|ZzyI<$7<`%_36Z;@2L=kw(KvDSJKD7d=&qfXc!7#Ny(^!Aw*>m#$mZv z!U^$LQmiuZJ}qN(RC)BWGA;;@EyELkXym>u^i~+5!Ay3TPWmqn4GlQsA(cT>BO4AF zCr*>6q1oygC}TquG-4GtlL3i}K?vuC_@F#WatR5nl5SJn=B}K>shH5{p8OlfBOBz3 ztz@y3T!(xxip)e3<%>H0^BWJ|(lc<`dVQE&ksV1{<}bp5xv;g~P+$QwYOkO!_Xnc= z*y#*}^-__CD9UQc%K={AwbYu?um*(uu;AzZa5e>s=#f9#l-@|?r&0HT8}j9-)i%a zFn+$Kh+7e^X5|GHw5rA1Rfaw8VQrmX_*avMMp3b;nRPmtmW0RKq*+B8l=#tanDQzp zTlYRbuo0XFibhjJY{D8^0n8yD_#i0JX96Rv>xGY~wC^a;hE|(mlG`pT-Fqod->My@ z%(J!m11WL1gs#^oDYoSvK|wb@;EnsiSvH@~9s3F1T^71cGF^vo%I=QmH^QEHY;$P5hiHBF3=rF?dc&!V|4&+q3dbf zTZii~eHZn4A4NMR`SyrT_?O&Tokf6ekXaAo9xQFJ-be6Yyc0n;ScHz6RyzVO|CzGj zF`3QSiayB2lzLJJb#v@0zJfKdSOv4E@PlZHb(@@P1rcupaxybzFxL6>Ogkm$8_&r6 zA4TkM>{hd;MU!`_6&l^6v9Rt{+{XmMOG!~4r4@$fflQZ867UGFEOnPfAjnYDM}l*BmV0d zF`r`aI4d@33yh&LkJ0?r6Vl6X5jLBjdVlU{Lm%A)sf&)nGR(Iak?^tKgqBHu;1vl= z5~TR~%Y%)8DaeHe9tNA%l+C0!2VQmLW$}>BLRlv-PG$&aEWNYB$oILec&nN>n z)nUW7v6>ajv&TtpI2msLXnS<1G%{hWykq$q@_1p#SKuFKchT+QFMLf_Sm$n-13mV} z4vyA*PHq^g>Jq}~YXJ{3M!lzQVYNZ%M^b6Qh)wGed;^NsH>ZadGdtz*<#XsarxE

D!I1{Rz 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≈kK5r4!2Copr|{}UV54CsWxJ_fI742yP3c!t+r{T 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

jh85;|DNbP7O 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 Date: Sat, 18 Jun 2022 09:17:16 +0800 Subject: [PATCH 021/116] docs: update documents --- README.md | 378 +++++++++++++++++++++--- README_zh.md | 405 ++++++++++++++++++++++---- examples/bar_chart/main.go | 2 +- examples/charts/main.go | 2 +- examples/chinese/main.go | 2 +- examples/funnel_chart/main.go | 2 +- examples/horizontal_bar_chart/main.go | 2 +- examples/line_chart/main.go | 2 +- examples/painter/main.go | 2 +- examples/pie_chart/main.go | 2 +- examples/radar_chart/main.go | 2 +- go.mod | 8 +- go.sum | 15 +- 13 files changed, 695 insertions(+), 129 deletions(-) diff --git a/README.md b/README.md index 7affa30..a58adb4 100644 --- a/README.md +++ b/README.md @@ -21,35 +21,19 @@ These chart types are supported: `line`, `bar`, `pie`, `radar` or `funnel`. ## Example -The example is for `golang option` and `echarts option`, more examples can be found in the `./examples/` directory. +More examples can be found in the [./examples/](./examples/) directory. + +### Line Chart ```go package main import ( - "os" - "path/filepath" - - charts "github.com/vicanso/go-charts" + charts "github.com/vicanso/go-charts/v2" ) -func writeFile(file string, buf []byte) error { - tmpPath := "./tmp" - err := os.MkdirAll(tmpPath, 0700) - if err != nil { - return err - } - - file = filepath.Join(tmpPath, file) - err = os.WriteFile(file, buf, 0600) - if err != nil { - return err - } - return nil -} - -func chartsRender() ([]byte, error) { - values := [][]float64{ +func main() { +values := [][]float64{ { 120, 132, @@ -59,6 +43,18 @@ func chartsRender() ([]byte, error) { 230, 210, }, + { + // snip... + }, + { + // snip... + }, + { + // snip... + }, + { + // snip... + }, } p, err := charts.LineRender( values, @@ -72,15 +68,323 @@ func chartsRender() ([]byte, error) { "Sat", "Sun", }), + charts.LegendLabelsOptionFunc([]string{ + "Email", + "Union Ads", + "Video Ads", + "Direct", + "Search Engine", + }, charts.PositionCenter), + ) + + if err != nil { + panic(err) + } + + buf, err := p.Bytes() + if err != nil { + panic(err) + } + // snip... +} +``` + +### Bar Chart + +```go +package main + +import ( + "github.com/vicanso/go-charts/v2" +) + +func main() { + values := [][]float64{ + { + 2.0, + 4.9, + 7.0, + 23.2, + 25.6, + 76.7, + 135.6, + 162.2, + 32.6, + 20.0, + 6.4, + 3.3, + }, + { + // snip... + }, + } + p, err := charts.BarRender( + values, + charts.XAxisDataOptionFunc([]string{ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + }), + charts.LegendLabelsOptionFunc([]string{ + "Rainfall", + "Evaporation", + }, charts.PositionRight), + charts.MarkLineOptionFunc(0, charts.SeriesMarkDataTypeAverage), + charts.MarkPointOptionFunc(0, charts.SeriesMarkDataTypeMax, + charts.SeriesMarkDataTypeMin), + // custom option func + func(opt *charts.ChartOption) { + opt.SeriesList[1].MarkPoint = charts.NewMarkPoint( + charts.SeriesMarkDataTypeMax, + charts.SeriesMarkDataTypeMin, + ) + opt.SeriesList[1].MarkLine = charts.NewMarkLine( + charts.SeriesMarkDataTypeAverage, + ) + }, ) if err != nil { - return nil, err + panic(err) } - return p.Bytes() -} -func echartsRender() ([]byte, error) { - return charts.RenderEChartsToPNG(`{ + buf, err := p.Bytes() + if err != nil { + panic(err) + } + // snip... +} +``` + +### Horizontal Bar Chart + +```go +package main + +import ( + "github.com/vicanso/go-charts/v2" +) + +func main() { + values := [][]float64{ + { + 18203, + 23489, + 29034, + 104970, + 131744, + 630230, + }, + { + // snip... + }, + } + p, err := charts.HorizontalBarRender( + values, + charts.TitleTextOptionFunc("World Population"), + charts.PaddingOptionFunc(charts.Box{ + Top: 20, + Right: 40, + Bottom: 20, + Left: 20, + }), + charts.LegendLabelsOptionFunc([]string{ + "2011", + "2012", + }), + charts.YAxisDataOptionFunc([]string{ + "Brazil", + "Indonesia", + "USA", + "India", + "China", + "World", + }), + ) + if err != nil { + panic(err) + } + + buf, err := p.Bytes() + if err != nil { + panic(err) + } + // snip... +} +``` + +### Pie Chart + +```go +package main + +import ( + "github.com/vicanso/go-charts/v2" +) + +func main() { + values := []float64{ + 1048, + 735, + 580, + 484, + 300, + } + p, err := charts.PieRender( + values, + charts.TitleOptionFunc(charts.TitleOption{ + Text: "Rainfall vs Evaporation", + Subtext: "Fake Data", + Left: charts.PositionCenter, + }), + charts.PaddingOptionFunc(charts.Box{ + Top: 20, + Right: 20, + Bottom: 20, + Left: 20, + }), + charts.LegendOptionFunc(charts.LegendOption{ + Orient: charts.OrientVertical, + Data: []string{ + "Search Engine", + "Direct", + "Email", + "Union Ads", + "Video Ads", + }, + Left: charts.PositionLeft, + }), + charts.PieSeriesShowLabel(), + ) + if err != nil { + panic(err) + } + + buf, err := p.Bytes() + if err != nil { + panic(err) + } + // snip... +} +``` + +### Radar Chart + +```go +package main + +import ( + "github.com/vicanso/go-charts/v2" +) + +func main() { + values := [][]float64{ + { + 4200, + 3000, + 20000, + 35000, + 50000, + 18000, + }, + { + // snip... + }, + } + p, err := charts.RadarRender( + values, + charts.TitleTextOptionFunc("Basic Radar Chart"), + charts.LegendLabelsOptionFunc([]string{ + "Allocated Budget", + "Actual Spending", + }), + charts.RadarIndicatorOptionFunc([]string{ + "Sales", + "Administration", + "Information Technology", + "Customer Support", + "Development", + "Marketing", + }, []float64{ + 6500, + 16000, + 30000, + 38000, + 52000, + 25000, + }), + ) + if err != nil { + panic(err) + } + + buf, err := p.Bytes() + if err != nil { + panic(err) + } + // snip... +} +``` + +### Funnel Chart + +```go +package main + +import ( + "github.com/vicanso/go-charts/v2" +) + +func main() { + values := []float64{ + 100, + 80, + 60, + 40, + 20, + } + p, err := charts.FunnelRender( + values, + charts.TitleTextOptionFunc("Funnel"), + charts.LegendLabelsOptionFunc([]string{ + "Show", + "Click", + "Visit", + "Inquiry", + "Order", + }), + ) + if err != nil { + panic(err) + } + + buf, err := p.Bytes() + if err != nil { + panic(err) + } + // snip... +} +``` + +### ECharts Render + +```go +package main + +import ( + "github.com/vicanso/go-charts/v2" +) + +func main() { + buf, err := charts.RenderEChartsToPNG(`{ "title": { "text": "Line" }, @@ -93,25 +397,7 @@ func echartsRender() ([]byte, error) { } ] }`) -} - -type Render func() ([]byte, error) - -func main() { - m := map[string]Render{ - "charts-line.png": chartsRender, - "echarts-line.png": echartsRender, - } - for name, fn := range m { - buf, err := fn() - if err != nil { - panic(err) - } - err = writeFile(name, buf) - if err != nil { - panic(err) - } - } + // snip... } ``` diff --git a/README_zh.md b/README_zh.md index 2d16b04..0e883bf 100644 --- a/README_zh.md +++ b/README_zh.md @@ -21,53 +21,44 @@ 下面的示例为`go-charts`两种方式的参数配置:golang的参数配置、echarts的JSON配置,输出相同的折线图。 -更多的示例参考:`./examples/`目录 +更多的示例参考:[./examples/](./examples/)目录 +### Line Chart ```go package main import ( - "os" - "path/filepath" - - charts "github.com/vicanso/go-charts" + charts "github.com/vicanso/go-charts/v2" ) -func writeFile(file string, buf []byte) error { - tmpPath := "./tmp" - err := os.MkdirAll(tmpPath, 0700) - if err != nil { - return err - } - - file = filepath.Join(tmpPath, file) - err = os.WriteFile(file, buf, 0600) - if err != nil { - return err - } - return nil -} - -func chartsRender() ([]byte, error) { - d, err := charts.LineRender([][]float64{ +func main() { +values := [][]float64{ { - 150, + 120, + 132, + 101, + 134, + 90, 230, - 224, - 218, - 135, - 147, - 260, + 210, }, - }, - // output type - charts.PNGTypeOption(), - // title - charts.TitleOptionFunc(charts.TitleOption{ - Text: "Line", - }), - // x axis - charts.XAxisOptionFunc(charts.NewXAxisOption([]string{ + { + // snip... + }, + { + // snip... + }, + { + // snip... + }, + { + // snip... + }, + } + p, err := charts.LineRender( + values, + charts.TitleTextOptionFunc("Line"), + charts.XAxisDataOptionFunc([]string{ "Mon", "Tue", "Wed", @@ -75,16 +66,324 @@ func chartsRender() ([]byte, error) { "Fri", "Sat", "Sun", - })), + }), + charts.LegendLabelsOptionFunc([]string{ + "Email", + "Union Ads", + "Video Ads", + "Direct", + "Search Engine", + }, charts.PositionCenter), + ) + + if err != nil { + panic(err) + } + + buf, err := p.Bytes() + if err != nil { + panic(err) + } + // snip... +} +``` + +### Bar Chart + +```go +package main + +import ( + "github.com/vicanso/go-charts/v2" +) + +func main() { + values := [][]float64{ + { + 2.0, + 4.9, + 7.0, + 23.2, + 25.6, + 76.7, + 135.6, + 162.2, + 32.6, + 20.0, + 6.4, + 3.3, + }, + { + // snip... + }, + } + p, err := charts.BarRender( + values, + charts.XAxisDataOptionFunc([]string{ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + }), + charts.LegendLabelsOptionFunc([]string{ + "Rainfall", + "Evaporation", + }, charts.PositionRight), + charts.MarkLineOptionFunc(0, charts.SeriesMarkDataTypeAverage), + charts.MarkPointOptionFunc(0, charts.SeriesMarkDataTypeMax, + charts.SeriesMarkDataTypeMin), + // custom option func + func(opt *charts.ChartOption) { + opt.SeriesList[1].MarkPoint = charts.NewMarkPoint( + charts.SeriesMarkDataTypeMax, + charts.SeriesMarkDataTypeMin, + ) + opt.SeriesList[1].MarkLine = charts.NewMarkLine( + charts.SeriesMarkDataTypeAverage, + ) + }, ) if err != nil { - return nil, err + panic(err) } - return d.Bytes() -} -func echartsRender() ([]byte, error) { - return charts.RenderEChartsToPNG(`{ + buf, err := p.Bytes() + if err != nil { + panic(err) + } + // snip... +} +``` + +### Horizontal Bar Chart + +```go +package main + +import ( + "github.com/vicanso/go-charts/v2" +) + +func main() { + values := [][]float64{ + { + 18203, + 23489, + 29034, + 104970, + 131744, + 630230, + }, + { + // snip... + }, + } + p, err := charts.HorizontalBarRender( + values, + charts.TitleTextOptionFunc("World Population"), + charts.PaddingOptionFunc(charts.Box{ + Top: 20, + Right: 40, + Bottom: 20, + Left: 20, + }), + charts.LegendLabelsOptionFunc([]string{ + "2011", + "2012", + }), + charts.YAxisDataOptionFunc([]string{ + "Brazil", + "Indonesia", + "USA", + "India", + "China", + "World", + }), + ) + if err != nil { + panic(err) + } + + buf, err := p.Bytes() + if err != nil { + panic(err) + } + // snip... +} +``` + +### Pie Chart + +```go +package main + +import ( + "github.com/vicanso/go-charts/v2" +) + +func main() { + values := []float64{ + 1048, + 735, + 580, + 484, + 300, + } + p, err := charts.PieRender( + values, + charts.TitleOptionFunc(charts.TitleOption{ + Text: "Rainfall vs Evaporation", + Subtext: "Fake Data", + Left: charts.PositionCenter, + }), + charts.PaddingOptionFunc(charts.Box{ + Top: 20, + Right: 20, + Bottom: 20, + Left: 20, + }), + charts.LegendOptionFunc(charts.LegendOption{ + Orient: charts.OrientVertical, + Data: []string{ + "Search Engine", + "Direct", + "Email", + "Union Ads", + "Video Ads", + }, + Left: charts.PositionLeft, + }), + charts.PieSeriesShowLabel(), + ) + if err != nil { + panic(err) + } + + buf, err := p.Bytes() + if err != nil { + panic(err) + } + // snip... +} +``` + +### Radar Chart + +```go +package main + +import ( + "github.com/vicanso/go-charts/v2" +) + +func main() { + values := [][]float64{ + { + 4200, + 3000, + 20000, + 35000, + 50000, + 18000, + }, + { + // snip... + }, + } + p, err := charts.RadarRender( + values, + charts.TitleTextOptionFunc("Basic Radar Chart"), + charts.LegendLabelsOptionFunc([]string{ + "Allocated Budget", + "Actual Spending", + }), + charts.RadarIndicatorOptionFunc([]string{ + "Sales", + "Administration", + "Information Technology", + "Customer Support", + "Development", + "Marketing", + }, []float64{ + 6500, + 16000, + 30000, + 38000, + 52000, + 25000, + }), + ) + if err != nil { + panic(err) + } + + buf, err := p.Bytes() + if err != nil { + panic(err) + } + // snip... +} +``` + +### Funnel Chart + +```go +package main + +import ( + "github.com/vicanso/go-charts/v2" +) + +func main() { + values := []float64{ + 100, + 80, + 60, + 40, + 20, + } + p, err := charts.FunnelRender( + values, + charts.TitleTextOptionFunc("Funnel"), + charts.LegendLabelsOptionFunc([]string{ + "Show", + "Click", + "Visit", + "Inquiry", + "Order", + }), + ) + if err != nil { + panic(err) + } + + buf, err := p.Bytes() + if err != nil { + panic(err) + } + // snip... +} +``` + +### ECharts Render + +```go +package main + +import ( + "github.com/vicanso/go-charts/v2" +) + +func main() { + buf, err := charts.RenderEChartsToPNG(`{ "title": { "text": "Line" }, @@ -97,25 +396,7 @@ func echartsRender() ([]byte, error) { } ] }`) -} - -type Render func() ([]byte, error) - -func main() { - m := map[string]Render{ - "charts-line.png": chartsRender, - "echarts-line.png": echartsRender, - } - for name, fn := range m { - buf, err := fn() - if err != nil { - panic(err) - } - err = writeFile(name, buf) - if err != nil { - panic(err) - } - } + // snip... } ``` diff --git a/examples/bar_chart/main.go b/examples/bar_chart/main.go index c9f1d58..c559a76 100644 --- a/examples/bar_chart/main.go +++ b/examples/bar_chart/main.go @@ -5,7 +5,7 @@ import ( "os" "path/filepath" - "github.com/vicanso/go-charts" + "github.com/vicanso/go-charts/v2" ) func writeFile(buf []byte) error { diff --git a/examples/charts/main.go b/examples/charts/main.go index b370b69..0e1d48e 100644 --- a/examples/charts/main.go +++ b/examples/charts/main.go @@ -5,7 +5,7 @@ import ( "net/http" "strconv" - charts "github.com/vicanso/go-charts" + charts "github.com/vicanso/go-charts/v2" ) var html = ` diff --git a/examples/chinese/main.go b/examples/chinese/main.go index 13724aa..bb7cc00 100644 --- a/examples/chinese/main.go +++ b/examples/chinese/main.go @@ -5,7 +5,7 @@ import ( "os" "path/filepath" - "github.com/vicanso/go-charts" + "github.com/vicanso/go-charts/v2" ) func writeFile(buf []byte) error { diff --git a/examples/funnel_chart/main.go b/examples/funnel_chart/main.go index 6b17614..8f21db6 100644 --- a/examples/funnel_chart/main.go +++ b/examples/funnel_chart/main.go @@ -5,7 +5,7 @@ import ( "os" "path/filepath" - "github.com/vicanso/go-charts" + "github.com/vicanso/go-charts/v2" ) func writeFile(buf []byte) error { diff --git a/examples/horizontal_bar_chart/main.go b/examples/horizontal_bar_chart/main.go index 6b206b0..8b996b6 100644 --- a/examples/horizontal_bar_chart/main.go +++ b/examples/horizontal_bar_chart/main.go @@ -5,7 +5,7 @@ import ( "os" "path/filepath" - "github.com/vicanso/go-charts" + "github.com/vicanso/go-charts/v2" ) func writeFile(buf []byte) error { diff --git a/examples/line_chart/main.go b/examples/line_chart/main.go index 435da78..45ff894 100644 --- a/examples/line_chart/main.go +++ b/examples/line_chart/main.go @@ -5,7 +5,7 @@ import ( "os" "path/filepath" - "github.com/vicanso/go-charts" + "github.com/vicanso/go-charts/v2" ) func writeFile(buf []byte) error { diff --git a/examples/painter/main.go b/examples/painter/main.go index cf2bb81..304361d 100644 --- a/examples/painter/main.go +++ b/examples/painter/main.go @@ -5,7 +5,7 @@ import ( "os" "path/filepath" - charts "github.com/vicanso/go-charts" + charts "github.com/vicanso/go-charts/v2" "github.com/wcharczuk/go-chart/v2/drawing" ) diff --git a/examples/pie_chart/main.go b/examples/pie_chart/main.go index 8a98e57..3721ed1 100644 --- a/examples/pie_chart/main.go +++ b/examples/pie_chart/main.go @@ -5,7 +5,7 @@ import ( "os" "path/filepath" - "github.com/vicanso/go-charts" + "github.com/vicanso/go-charts/v2" ) func writeFile(buf []byte) error { diff --git a/examples/radar_chart/main.go b/examples/radar_chart/main.go index 9550951..51f7409 100644 --- a/examples/radar_chart/main.go +++ b/examples/radar_chart/main.go @@ -5,7 +5,7 @@ import ( "os" "path/filepath" - "github.com/vicanso/go-charts" + "github.com/vicanso/go-charts/v2" ) func writeFile(buf []byte) error { diff --git a/go.mod b/go.mod index 610af22..66145c7 100644 --- a/go.mod +++ b/go.mod @@ -1,17 +1,17 @@ -module github.com/vicanso/go-charts +module github.com/vicanso/go-charts/v2 go 1.17 require ( github.com/dustin/go-humanize v1.0.0 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 - github.com/stretchr/testify v1.7.1 + github.com/stretchr/testify v1.7.2 github.com/wcharczuk/go-chart/v2 v2.1.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9 // indirect - gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect + golang.org/x/image v0.0.0-20220617043117-41969df76e82 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index d88f473..5f953b0 100644 --- a/go.sum +++ b/go.sum @@ -8,18 +8,17 @@ github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGw github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/wcharczuk/go-chart/v2 v2.1.0 h1:tY2slqVQ6bN+yHSnDYwZebLQFkphK4WNrVwnt7CJZ2I= github.com/wcharczuk/go-chart/v2 v2.1.0/go.mod h1:yx7MvAVNcP/kN9lKXM/NTce4au4DFN99j6i1OwDclNA= golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9 h1:LRtI4W37N+KFebI/qV0OFiLUv4GLOWeEW5hn/KEJvxE= -golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/image v0.0.0-20220617043117-41969df76e82 h1:KpZB5pUSBvrHltNEdK/tw0xlPeD13M6M6aGP32gKqiw= +golang.org/x/image v0.0.0-20220617043117-41969df76e82/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From d3c6649cd9e152daf3f6a7ea0780825db2695c17 Mon Sep 17 00:00:00 2001 From: vicanso Date: Sat, 18 Jun 2022 10:38:46 +0800 Subject: [PATCH 022/116] test: add test for axis --- README.md | 2 +- README_zh.md | 2 +- axis.go | 7 ++- axis_test.go | 129 +++++++++++++++++++++++++++++++++++++++ examples/chinese/main.go | 5 +- examples/painter/main.go | 6 +- legend.go | 2 +- line_chart.go | 2 +- mark_line.go | 10 --- painter.go | 4 +- range.go | 10 --- series.go | 6 -- 12 files changed, 145 insertions(+), 40 deletions(-) create mode 100644 axis_test.go diff --git a/README.md b/README.md index a58adb4..8183871 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ import ( ) func main() { -values := [][]float64{ + values := [][]float64{ { 120, 132, diff --git a/README_zh.md b/README_zh.md index 9c0be5b..fed2d61 100644 --- a/README_zh.md +++ b/README_zh.md @@ -32,7 +32,7 @@ import ( ) func main() { -values := [][]float64{ + values := [][]float64{ { 120, 132, diff --git a/axis.go b/axis.go index aa7cf7d..00a3332 100644 --- a/axis.go +++ b/axis.go @@ -81,7 +81,10 @@ func (a *axisPainter) Render() (Box, error) { opt := a.opt top := a.p theme := opt.Theme - if opt.Show != nil && !*opt.Show { + if theme == nil { + theme = top.theme + } + if isFalse(opt.Show) { return BoxZero, nil } @@ -121,7 +124,7 @@ func (a *axisPainter) Render() (Box, error) { tickCount := dataCount boundaryGap := true - if opt.BoundaryGap != nil && !*opt.BoundaryGap { + if isFalse(opt.BoundaryGap) { boundaryGap = false } isVertical := opt.Position == PositionLeft || diff --git a/axis_test.go b/axis_test.go new file mode 100644 index 0000000..fe7f874 --- /dev/null +++ b/axis_test.go @@ -0,0 +1,129 @@ +// 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" + "github.com/wcharczuk/go-chart/v2/drawing" +) + +func TestAxis(t *testing.T) { + assert := assert.New(t) + tests := []struct { + render func(*Painter) ([]byte, error) + result string + }{ + // 底部x轴 + { + render: func(p *Painter) ([]byte, error) { + _, _ = NewAxisPainter(p, AxisOption{ + Data: []string{ + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun", + }, + }).Render() + return p.Bytes() + }, + result: "\\nMonTueWedThuFriSatSun", + }, + // 底部x轴文本居左 + { + render: func(p *Painter) ([]byte, error) { + _, _ = NewAxisPainter(p, AxisOption{ + Data: []string{ + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun", + }, + BoundaryGap: FalseFlag(), + }).Render() + return p.Bytes() + }, + result: "\\nMonTueWedThuFriSatSun", + }, + // 左侧y轴 + { + render: func(p *Painter) ([]byte, error) { + _, _ = NewAxisPainter(p, AxisOption{ + Data: []string{ + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun", + }, + Position: PositionLeft, + }).Render() + return p.Bytes() + }, + result: "\\nMonTueWedThuFriSatSun", + }, + // 左侧y轴居中 + { + render: func(p *Painter) ([]byte, error) { + _, _ = NewAxisPainter(p, AxisOption{ + Data: []string{ + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun", + }, + Position: PositionLeft, + BoundaryGap: FalseFlag(), + SplitLineShow: true, + SplitLineColor: drawing.ColorBlack, + }).Render() + return p.Bytes() + }, + result: "\\nMonTueWedThuFriSatSun", + }, + } + + for _, tt := range tests { + p, err := NewPainter(PainterOptions{ + Type: ChartOutputSVG, + Width: 600, + Height: 400, + }, PainterThemeOption(defaultTheme)) + assert.Nil(err) + data, err := tt.render(p) + assert.Nil(err) + assert.Equal(tt.result, string(data)) + } +} diff --git a/examples/chinese/main.go b/examples/chinese/main.go index 9c2d6a5..bb7cc00 100644 --- a/examples/chinese/main.go +++ b/examples/chinese/main.go @@ -114,6 +114,5 @@ func main() { err = writeFile(buf) if err != nil { panic(err) - } -) - + } +} diff --git a/examples/painter/main.go b/examples/painter/main.go index 304361d..3c31ce4 100644 --- a/examples/painter/main.go +++ b/examples/painter/main.go @@ -430,7 +430,7 @@ func main() { Left: 1, Right: p.Width() - 1, Bottom: top + 30, - })), charts.LegendPainterOption{ + })), charts.LegendOption{ Left: "10", Data: []string{ "Email", @@ -449,7 +449,7 @@ func main() { Left: 1, Right: p.Width() - 1, Bottom: top + 30, - })), charts.LegendPainterOption{ + })), charts.LegendOption{ Left: charts.PositionRight, Data: []string{ "Email", @@ -470,7 +470,7 @@ func main() { Left: 1, Right: p.Width() - 1, Bottom: top + 100, - })), charts.LegendPainterOption{ + })), charts.LegendOption{ Top: "10", Data: []string{ "Email", diff --git a/legend.go b/legend.go index cf8d417..65db102 100644 --- a/legend.go +++ b/legend.go @@ -92,7 +92,7 @@ func (l *legendPainter) Render() (Box, error) { opt := l.opt theme := opt.Theme if opt.IsEmpty() || - (opt.Show != nil && !*opt.Show) { + isFalse(opt.Show) { return BoxZero, nil } if theme == nil { diff --git a/line_chart.go b/line_chart.go index 0dc0fd8..f171813 100644 --- a/line_chart.go +++ b/line_chart.go @@ -66,7 +66,7 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) ( p := l.p opt := l.opt boundaryGap := true - if opt.XAxis.BoundaryGap != nil && !*opt.XAxis.BoundaryGap { + if isFalse(opt.XAxis.BoundaryGap) { boundaryGap = false } diff --git a/mark_line.go b/mark_line.go index bb1b602..00a37f2 100644 --- a/mark_line.go +++ b/mark_line.go @@ -105,13 +105,3 @@ func (m *markLinePainter) Render() (Box, error) { } return BoxZero, nil } - -func markLineRender(opt markLineRenderOption) { - // d := opt.Draw - // s := opt.Series - // if len(s.MarkLine.Data) == 0 { - // return - // } - // r := d.Render - -} diff --git a/painter.go b/painter.go index 0bacd3c..da07007 100644 --- a/painter.go +++ b/painter.go @@ -41,7 +41,7 @@ type Painter struct { } type PainterOptions struct { - // Draw type, "svg" or "png", default type is "svg" + // Draw type, "svg" or "png", default type is "png" Type string // The width of draw painter Width int @@ -628,7 +628,7 @@ func (p *Painter) MultiText(opt MultiTextOption) *Painter { values = autoDivide(width, count) } for index, text := range opt.TextList { - if index%opt.Unit != 0 { + if opt.Unit != 0 && index%opt.Unit != 0 { continue } box := p.MeasureText(text) diff --git a/range.go b/range.go index 399c449..d5a9ef7 100644 --- a/range.go +++ b/range.go @@ -115,13 +115,3 @@ func (r *axisRange) GetRange(index int) (float64, float64) { func (r *axisRange) AutoDivide() []int { return autoDivide(r.size, r.divideCount) } - -func (r *axisRange) 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)) -} diff --git a/series.go b/series.go index 905c140..44c4749 100644 --- a/series.go +++ b/series.go @@ -140,12 +140,6 @@ func (sl SeriesList) init() { } } -func (sl SeriesList) reverse() { - for i, j := 0, len(sl)-1; i < j; i, j = i+1, j-1 { - sl[i], sl[j] = sl[j], sl[i] - } -} - func (sl SeriesList) Filter(chartType string) SeriesList { arr := make(SeriesList, 0) for index, item := range sl { From 29c9281d7c878f438a37b27a89f27b47de25d44d Mon Sep 17 00:00:00 2001 From: vicanso Date: Sat, 18 Jun 2022 10:49:39 +0800 Subject: [PATCH 023/116] test: fix test --- painter_test.go | 3 +++ util_test.go | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/painter_test.go b/painter_test.go index c847aff..8892563 100644 --- a/painter_test.go +++ b/painter_test.go @@ -39,6 +39,7 @@ func TestPainterOption(t *testing.T) { d, err := NewPainter(PainterOptions{ Width: 800, Height: 600, + Type: ChartOutputSVG, }, PainterBoxOption(Box{ Right: 400, @@ -329,6 +330,7 @@ func TestPainter(t *testing.T) { d, err := NewPainter(PainterOptions{ Width: 400, Height: 300, + Type: ChartOutputSVG, }, PainterPaddingOption(chart.Box{ Left: 5, Top: 10, @@ -346,6 +348,7 @@ func TestPainterTextFit(t *testing.T) { p, err := NewPainter(PainterOptions{ Width: 400, Height: 300, + Type: ChartOutputSVG, }) assert.Nil(err) f, _ := chart.GetDefaultFont() diff --git a/util_test.go b/util_test.go index b25c60d..7c2ab2f 100644 --- a/util_test.go +++ b/util_test.go @@ -99,7 +99,7 @@ func TestMeasureTextMaxWidthHeight(t *testing.T) { "Sat", "Sun", }, p) - assert.Equal(26, maxWidth) + assert.Equal(31, maxWidth) assert.Equal(12, maxHeight) } From ad70a4894472b5b5b66c0d76b82cd753e2ebf354 Mon Sep 17 00:00:00 2001 From: vicanso Date: Sat, 18 Jun 2022 20:46:12 +0800 Subject: [PATCH 024/116] test: add test for funnel chart --- axis.go | 2 +- axis_test.go | 46 ++++++++++++++++- bar_chart.go | 22 +++++++- bar_chart_test.go | 120 +++++++++++++++++++++++++++++++++++++++++++ chart_option.go | 7 +-- funnel_chart.go | 10 ++++ funnel_chart_test.go | 79 ++++++++++++++++++++++++++++ 7 files changed, 276 insertions(+), 10 deletions(-) create mode 100644 bar_chart_test.go create mode 100644 funnel_chart_test.go diff --git a/axis.go b/axis.go index 00a3332..53b5362 100644 --- a/axis.go +++ b/axis.go @@ -194,7 +194,7 @@ func (a *axisPainter) Render() (Box, error) { switch opt.Position { case PositionTop: - labelPaddingTop = labelMargin + labelPaddingTop = 0 x1 = p.Width() y0 = labelMargin + int(opt.FontSize) ticksPaddingTop = int(opt.FontSize) diff --git a/axis_test.go b/axis_test.go index fe7f874..17fe8d6 100644 --- a/axis_test.go +++ b/axis_test.go @@ -48,10 +48,12 @@ func TestAxis(t *testing.T) { "Sat", "Sun", }, + SplitLineShow: true, + SplitLineColor: drawing.ColorBlack, }).Render() return p.Bytes() }, - result: "\\nMonTueWedThuFriSatSun", + result: "\\nMonTueWedThuFriSatSun", }, // 底部x轴文本居左 { @@ -113,6 +115,48 @@ func TestAxis(t *testing.T) { }, result: "\\nMonTueWedThuFriSatSun", }, + // 右侧 + { + render: func(p *Painter) ([]byte, error) { + _, _ = NewAxisPainter(p, AxisOption{ + Data: []string{ + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun", + }, + Position: PositionRight, + BoundaryGap: FalseFlag(), + SplitLineShow: true, + SplitLineColor: drawing.ColorBlack, + }).Render() + return p.Bytes() + }, + result: "\\nMonTueWedThuFriSatSun", + }, + // 顶部 + { + render: func(p *Painter) ([]byte, error) { + _, _ = NewAxisPainter(p, AxisOption{ + Data: []string{ + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun", + }, + Formatter: "{value} --", + Position: PositionTop, + }).Render() + return p.Bytes() + }, + result: "\\nMon --Tue --Wed --Thu --Fri --Sat --Sun --", + }, } for _, tt := range tests { diff --git a/bar_chart.go b/bar_chart.go index 2982829..0ac9f47 100644 --- a/bar_chart.go +++ b/bar_chart.go @@ -60,6 +60,13 @@ type BarChartOption struct { Legend LegendOption } +type barChartLabelRenderOption struct { + Text string + Style Style + X int + Y int +} + func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) { p := b.p opt := b.opt @@ -95,6 +102,7 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B markPointPainter, markLinePainter, } + labelRenderOptions := make([]barChartLabelRenderOption, 0) for index := range seriesList { series := seriesList[index] yRange := result.axisRanges[series.AxisIndex] @@ -156,9 +164,15 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B if !series.Label.Color.IsZero() { labelStyle.FontColor = series.Label.Color } - seriesPainter.OverrideTextStyle(labelStyle) + textBox := seriesPainter.MeasureText(text) - seriesPainter.Text(text, x+(barWidth-textBox.Width())>>1, barMaxHeight-h-distance) + + labelRenderOptions = append(labelRenderOptions, barChartLabelRenderOption{ + Text: text, + Style: labelStyle, + X: x + (barWidth-textBox.Width())>>1, + Y: barMaxHeight - h - distance, + }) } markPointPainter.Add(markPointRenderOption{ @@ -176,6 +190,10 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B Range: yRange, }) } + for _, labelOpt := range labelRenderOptions { + seriesPainter.OverrideTextStyle(labelOpt.Style) + seriesPainter.Text(labelOpt.Text, labelOpt.X, labelOpt.Y) + } // 最大、最小的mark point err := doRender(rendererList...) if err != nil { diff --git a/bar_chart_test.go b/bar_chart_test.go new file mode 100644 index 0000000..138b3ca --- /dev/null +++ b/bar_chart_test.go @@ -0,0 +1,120 @@ +// 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 TestBarChart(t *testing.T) { + assert := assert.New(t) + + tests := []struct { + render func(*Painter) ([]byte, error) + result string + }{ + { + render: func(p *Painter) ([]byte, error) { + seriesList := NewSeriesListDataFromValues([][]float64{ + { + 2.0, + 4.9, + 7.0, + 23.2, + 25.6, + 76.7, + 135.6, + 162.2, + 32.6, + 20.0, + 6.4, + 3.3, + }, + { + 2.6, + 5.9, + 9.0, + 26.4, + 28.7, + 70.7, + 175.6, + 182.2, + 48.7, + 18.8, + 6.0, + 2.3, + }, + }) + for index := range seriesList { + seriesList[index].Label.Show = true + } + _, err := NewBarChart(p, BarChartOption{ + Padding: Box{ + Left: 10, + Top: 10, + Right: 10, + Bottom: 10, + }, + SeriesList: seriesList, + XAxis: NewXAxisOption([]string{ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + }), + YAxisOptions: NewYAxisOptions([]string{ + "Rainfall", + "Evaporation", + }), + }).Render() + if err != nil { + return nil, err + } + return p.Bytes() + }, + result: "\\n24020016012080400JanFebMarAprMayJunJulAugSepOctNovDec24.9723.225.676.7135.6162.232.6206.43.32.65.9926.428.770.7175.6182.248.718.862.3", + }, + } + + for _, tt := range tests { + p, err := NewPainter(PainterOptions{ + Type: ChartOutputSVG, + Width: 600, + Height: 400, + }, PainterThemeOption(defaultTheme)) + assert.Nil(err) + data, err := tt.render(p) + assert.Nil(err) + assert.Equal(tt.result, string(data)) + } +} diff --git a/chart_option.go b/chart_option.go index 0bc0a34..643c4e7 100644 --- a/chart_option.go +++ b/chart_option.go @@ -336,12 +336,7 @@ func RadarRender(values [][]float64, opts ...OptionFunc) (*Painter, error) { // FunnelRender funnel chart render func FunnelRender(values []float64, opts ...OptionFunc) (*Painter, error) { - seriesList := make(SeriesList, len(values)) - for index, value := range values { - seriesList[index] = NewSeriesFromValues([]float64{ - value, - }, ChartTypeFunnel) - } + seriesList := NewFunnelSeriesList(values) return Render(ChartOption{ SeriesList: seriesList, }, opts...) diff --git a/funnel_chart.go b/funnel_chart.go index 63b3504..7c04bfe 100644 --- a/funnel_chart.go +++ b/funnel_chart.go @@ -34,6 +34,16 @@ type funnelChart struct { opt *FunnelChartOption } +func NewFunnelSeriesList(values []float64) SeriesList { + seriesList := make(SeriesList, len(values)) + for index, value := range values { + seriesList[index] = NewSeriesFromValues([]float64{ + value, + }, ChartTypeFunnel) + } + return seriesList +} + func NewFunnelChart(p *Painter, opt FunnelChartOption) *funnelChart { if opt.Theme == nil { opt.Theme = defaultTheme diff --git a/funnel_chart_test.go b/funnel_chart_test.go new file mode 100644 index 0000000..d260bfb --- /dev/null +++ b/funnel_chart_test.go @@ -0,0 +1,79 @@ +// 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 TestFunnelChart(t *testing.T) { + assert := assert.New(t) + + tests := []struct { + render func(*Painter) ([]byte, error) + result string + }{ + { + render: func(p *Painter) ([]byte, error) { + _, err := NewFunnelChart(p, FunnelChartOption{ + SeriesList: NewFunnelSeriesList([]float64{ + 100, + 80, + 60, + 40, + 20, + }), + Legend: NewLegendOption([]string{ + "Show", + "Click", + "Visit", + "Inquiry", + "Order", + }), + Title: TitleOption{ + Text: "Funnel", + }, + }).Render() + if err != nil { + return nil, err + } + return p.Bytes() + }, + result: "\\nShowClickVisitInquiryOrderFunnel(100%)(80%)(60%)(40%)(20%)", + }, + } + + for _, tt := range tests { + p, err := NewPainter(PainterOptions{ + Type: ChartOutputSVG, + Width: 600, + Height: 400, + }, PainterThemeOption(defaultTheme)) + assert.Nil(err) + data, err := tt.render(p) + assert.Nil(err) + assert.Equal(tt.result, string(data)) + } +} From 368add795f8832f7397fce743141b9921d3a149d Mon Sep 17 00:00:00 2001 From: vicanso Date: Sun, 19 Jun 2022 19:28:09 +0800 Subject: [PATCH 025/116] test: add test for horizontal bar --- horizontal_bar_chart_test.go | 100 +++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 horizontal_bar_chart_test.go diff --git a/horizontal_bar_chart_test.go b/horizontal_bar_chart_test.go new file mode 100644 index 0000000..5555df6 --- /dev/null +++ b/horizontal_bar_chart_test.go @@ -0,0 +1,100 @@ +// 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 TestHorizontalBarChart(t *testing.T) { + assert := assert.New(t) + + tests := []struct { + render func(*Painter) ([]byte, error) + result string + }{ + { + render: func(p *Painter) ([]byte, error) { + _, err := NewHorizontalBarChart(p, HorizontalBarChartOption{ + Padding: Box{ + Top: 10, + Right: 10, + Bottom: 10, + Left: 10, + }, + SeriesList: NewSeriesListDataFromValues([][]float64{ + { + 18203, + 23489, + 29034, + 104970, + 131744, + 630230, + }, + { + 19325, + 23438, + 31000, + 121594, + 134141, + 681807, + }, + }, ChartTypeHorizontalBar), + Title: TitleOption{ + Text: "World Population", + }, + Legend: NewLegendOption([]string{ + "2011", + "2012", + }), + YAxisOptions: NewYAxisOptions([]string{ + "Brazil", + "Indonesia", + "USA", + "India", + "China", + "World", + }), + }).Render() + if err != nil { + return nil, err + } + return p.Bytes() + }, + result: "\\n20112012World PopulationWorldChinaIndiaUSAIndonesiaBrazil0122.28k244.56k366.84k489.12k611.4k733.68k", + }, + } + for _, tt := range tests { + p, err := NewPainter(PainterOptions{ + Type: ChartOutputSVG, + Width: 600, + Height: 400, + }, PainterThemeOption(defaultTheme)) + assert.Nil(err) + data, err := tt.render(p) + assert.Nil(err) + assert.Equal(tt.result, string(data)) + } +} From a6b92f1d4746c6e5d6350f627160961399cf7283 Mon Sep 17 00:00:00 2001 From: vicanso Date: Sun, 19 Jun 2022 20:02:54 +0800 Subject: [PATCH 026/116] test: add test for legend --- legend_test.go | 107 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 legend_test.go diff --git a/legend_test.go b/legend_test.go new file mode 100644 index 0000000..e10b538 --- /dev/null +++ b/legend_test.go @@ -0,0 +1,107 @@ +// 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 ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewLegend(t *testing.T) { + assert := assert.New(t) + + tests := []struct { + render func(*Painter) ([]byte, error) + result string + }{ + { + render: func(p *Painter) ([]byte, error) { + _, err := NewLegendPainter(p, LegendOption{ + Data: []string{ + "One", + "Two", + "Three", + }, + }).Render() + if err != nil { + return nil, err + } + return p.Bytes() + }, + result: "\\nOneTwoThree", + }, + { + render: func(p *Painter) ([]byte, error) { + _, err := NewLegendPainter(p, LegendOption{ + Data: []string{ + "One", + "Two", + "Three", + }, + Left: PositionLeft, + }).Render() + if err != nil { + return nil, err + } + return p.Bytes() + }, + result: "\\nOneTwoThree", + }, + { + render: func(p *Painter) ([]byte, error) { + _, err := NewLegendPainter(p, LegendOption{ + Data: []string{ + "One", + "Two", + "Three", + }, + Orient: OrientVertical, + Icon: IconRect, + Left: "10%", + }).Render() + if err != nil { + return nil, err + } + return p.Bytes() + }, + result: "\\nOneTwoThree", + }, + } + for index, tt := range tests { + if index != 0 { + continue + } + p, err := NewPainter(PainterOptions{ + Type: ChartOutputSVG, + Width: 600, + Height: 400, + }, PainterThemeOption(defaultTheme)) + assert.Nil(err) + data, err := tt.render(p) + assert.Nil(err) + fmt.Println(string(data)) + assert.Equal(tt.result, string(data)) + } +} From 212a51083f290aaa7770b7762ea8ee0992b24b5f Mon Sep 17 00:00:00 2001 From: vicanso Date: Mon, 20 Jun 2022 23:23:21 +0800 Subject: [PATCH 027/116] test: add test for charts --- chart_option.go | 12 +- echarts_test.go | 263 ++++++++++++++++++++++++++++++++++++++++++++ grid_test.go | 69 ++++++++++++ legend_test.go | 5 +- line_chart_test.go | 219 ++++++++++++++++++++++++++++++++++++ mark_line.go | 7 +- mark_line_test.go | 89 +++++++++++++++ mark_point_test.go | 92 ++++++++++++++++ pie_chart_test.go | 100 +++++++++++++++++ radar_chart.go | 14 +++ radar_chart_test.go | 109 ++++++++++++++++++ 11 files changed, 963 insertions(+), 16 deletions(-) create mode 100644 echarts_test.go create mode 100644 grid_test.go create mode 100644 line_chart_test.go create mode 100644 mark_line_test.go create mode 100644 mark_point_test.go create mode 100644 pie_chart_test.go create mode 100644 radar_chart_test.go diff --git a/chart_option.go b/chart_option.go index 643c4e7..db2f57b 100644 --- a/chart_option.go +++ b/chart_option.go @@ -201,17 +201,7 @@ func ChildOptionFunc(child ...ChartOption) OptionFunc { // RadarIndicatorOptionFunc set radar indicator of chart func RadarIndicatorOptionFunc(names []string, values []float64) OptionFunc { return func(opt *ChartOption) { - if len(names) != len(values) { - return - } - indicators := make([]RadarIndicator, len(names)) - for index, name := range names { - indicators[index] = RadarIndicator{ - Name: name, - Max: values[index], - } - } - opt.RadarIndicators = indicators + opt.RadarIndicators = NewRadarIndicators(names, values) } } diff --git a/echarts_test.go b/echarts_test.go new file mode 100644 index 0000000..1ed14d3 --- /dev/null +++ b/echarts_test.go @@ -0,0 +1,263 @@ +// 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 ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestConvertToArray(t *testing.T) { + assert := assert.New(t) + + assert.Equal([]byte(`[1]`), convertToArray([]byte("1"))) + assert.Equal([]byte(`[1]`), convertToArray([]byte("[1]"))) +} + +func TestEChartsPosition(t *testing.T) { + assert := assert.New(t) + var p EChartsPosition + err := p.UnmarshalJSON([]byte("1")) + assert.Nil(err) + assert.Equal(EChartsPosition("1"), p) + err = p.UnmarshalJSON([]byte(`"left"`)) + assert.Nil(err) + assert.Equal(EChartsPosition("left"), p) +} + +func TestEChartsSeriesDataValue(t *testing.T) { + assert := assert.New(t) + + es := EChartsSeriesDataValue{} + err := es.UnmarshalJSON([]byte(`[1, 2]`)) + assert.Nil(err) + assert.Equal(EChartsSeriesDataValue{ + values: []float64{ + 1, + 2, + }, + }, es) + assert.Equal(1.0, es.First()) +} + +func TestEChartsSeriesData(t *testing.T) { + assert := assert.New(t) + es := EChartsSeriesData{} + err := es.UnmarshalJSON([]byte("1.1")) + assert.Nil(err) + assert.Equal(EChartsSeriesDataValue{ + values: []float64{ + 1.1, + }, + }, es.Value) + + err = es.UnmarshalJSON([]byte(`{"value":200,"itemStyle":{"color":"#a90000"}}`)) + assert.Nil(err) + assert.Nil(err) + assert.Equal(EChartsSeriesData{ + Value: EChartsSeriesDataValue{ + values: []float64{ + 200.0, + }, + }, + ItemStyle: EChartStyle{ + Color: "#a90000", + }, + }, es) +} + +func TestEChartsXAxis(t *testing.T) { + assert := assert.New(t) + ex := EChartsXAxis{} + err := ex.UnmarshalJSON([]byte(`{"boundaryGap": true, "splitNumber": 5, "data": ["a", "b"], "type": "value"}`)) + assert.Nil(err) + + assert.Equal(EChartsXAxis{ + Data: []EChartsXAxisData{ + { + BoundaryGap: TrueFlag(), + SplitNumber: 5, + Data: []string{ + "a", + "b", + }, + Type: "value", + }, + }, + }, ex) +} + +func TestEChartsOption(t *testing.T) { + assert := assert.New(t) + + opt := EChartsOption{} + err := json.Unmarshal([]byte(`{ + "title": { + "text": "Rainfall vs Evaporation", + "subtext": "Fake Data" + }, + "tooltip": { + "trigger": "axis" + }, + "legend": { + "data": [ + "Rainfall", + "Evaporation" + ] + }, + "toolbox": { + "show": true, + "feature": { + "dataView": { + "show": true, + "readOnly": false + }, + "magicType": { + "show": true, + "type": [ + "line", + "bar" + ] + }, + "restore": { + "show": true + }, + "saveAsImage": { + "show": true + } + } + }, + "calculable": true, + "xAxis": [ + { + "type": "category", + "data": [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec" + ] + } + ], + "yAxis": [ + { + "type": "value" + } + ], + "series": [ + { + "name": "Rainfall", + "type": "bar", + "data": [ + 2, + 4.9, + 7, + 23.2, + 25.6, + 76.7, + 135.6, + 162.2, + 32.6, + 20, + 6.4, + 3.3 + ], + "markPoint": { + "data": [ + { + "type": "max", + "name": "Max" + }, + { + "type": "min", + "name": "Min" + } + ] + }, + "markLine": { + "data": [ + { + "type": "average", + "name": "Avg" + } + ] + } + }, + { + "name": "Evaporation", + "type": "bar", + "data": [ + 2.6, + 5.9, + 9, + 26.4, + 28.7, + 70.7, + 175.6, + 182.2, + 48.7, + 18.8, + 6, + 2.3 + ], + "markPoint": { + "data": [ + { + "name": "Max", + "value": 182.2, + "xAxis": 7, + "yAxis": 183 + }, + { + "name": "Min", + "value": 2.3, + "xAxis": 11, + "yAxis": 3 + } + ] + }, + "markLine": { + "data": [ + { + "type": "average", + "name": "Avg" + } + ] + } + } + ] + }`), &opt) + + assert.Nil(err) + assert.NotEmpty(opt.Series) +} diff --git a/grid_test.go b/grid_test.go new file mode 100644 index 0000000..f6880dc --- /dev/null +++ b/grid_test.go @@ -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 ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/wcharczuk/go-chart/v2/drawing" +) + +func TestGrid(t *testing.T) { + assert := assert.New(t) + + tests := []struct { + render func(*Painter) ([]byte, error) + result string + }{ + { + render: func(p *Painter) ([]byte, error) { + _, err := NewGridPainter(p, GridPainterOption{ + StrokeColor: drawing.ColorBlack, + Column: 6, + Row: 6, + IgnoreFirstRow: true, + IgnoreLastRow: true, + IgnoreFirstColumn: true, + IgnoreLastColumn: true, + }).Render() + if err != nil { + return nil, err + } + return p.Bytes() + }, + result: "\\n", + }, + } + for _, tt := range tests { + p, err := NewPainter(PainterOptions{ + Type: ChartOutputSVG, + Width: 600, + Height: 400, + }, PainterThemeOption(defaultTheme)) + assert.Nil(err) + data, err := tt.render(p) + assert.Nil(err) + assert.Equal(tt.result, string(data)) + } +} diff --git a/legend_test.go b/legend_test.go index e10b538..9078006 100644 --- a/legend_test.go +++ b/legend_test.go @@ -89,10 +89,7 @@ func TestNewLegend(t *testing.T) { result: "\\nOneTwoThree", }, } - for index, tt := range tests { - if index != 0 { - continue - } + for _, tt := range tests { p, err := NewPainter(PainterOptions{ Type: ChartOutputSVG, Width: 600, diff --git a/line_chart_test.go b/line_chart_test.go new file mode 100644 index 0000000..856cdf3 --- /dev/null +++ b/line_chart_test.go @@ -0,0 +1,219 @@ +// 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 TestLineChart(t *testing.T) { + assert := assert.New(t) + + tests := []struct { + render func(*Painter) ([]byte, error) + result string + }{ + { + render: func(p *Painter) ([]byte, error) { + values := [][]float64{ + { + 120, + 132, + 101, + 134, + 90, + 230, + 210, + }, + { + 220, + 182, + 191, + 234, + 290, + 330, + 310, + }, + { + 150, + 232, + 201, + 154, + 190, + 330, + 410, + }, + { + 320, + 332, + 301, + 334, + 390, + 330, + 320, + }, + { + 820, + 932, + 901, + 934, + 1290, + 1330, + 1320, + }, + } + _, err := NewLineChart(p, LineChartOption{ + Title: TitleOption{ + Text: "Line", + }, + Padding: Box{ + Top: 10, + Right: 10, + Bottom: 10, + Left: 10, + }, + XAxis: NewXAxisOption([]string{ + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun", + }), + Legend: NewLegendOption([]string{ + "Email", + "Union Ads", + "Video Ads", + "Direct", + "Search Engine", + }, PositionCenter), + SeriesList: NewSeriesListDataFromValues(values), + }).Render() + if err != nil { + return nil, err + } + return p.Bytes() + }, + result: "\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", + }, + { + render: func(p *Painter) ([]byte, error) { + values := [][]float64{ + { + 120, + 132, + 101, + 134, + 90, + 230, + 210, + }, + { + 220, + 182, + 191, + 234, + 290, + 330, + 310, + }, + { + 150, + 232, + 201, + 154, + 190, + 330, + 410, + }, + { + 320, + 332, + 301, + 334, + 390, + 330, + 320, + }, + { + 820, + 932, + 901, + 934, + 1290, + 1330, + 1320, + }, + } + _, err := NewLineChart(p, LineChartOption{ + Title: TitleOption{ + Text: "Line", + }, + Padding: Box{ + Top: 10, + Right: 10, + Bottom: 10, + Left: 10, + }, + XAxis: NewXAxisOption([]string{ + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun", + }, FalseFlag()), + Legend: NewLegendOption([]string{ + "Email", + "Union Ads", + "Video Ads", + "Direct", + "Search Engine", + }, PositionCenter), + SeriesList: NewSeriesListDataFromValues(values), + }).Render() + if err != nil { + return nil, err + } + return p.Bytes() + }, + result: "\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", + }, + } + + for _, tt := range tests { + p, err := NewPainter(PainterOptions{ + Type: ChartOutputSVG, + Width: 600, + Height: 400, + }, PainterThemeOption(defaultTheme)) + assert.Nil(err) + data, err := tt.render(p) + assert.Nil(err) + assert.Equal(tt.result, string(data)) + } +} diff --git a/mark_line.go b/mark_line.go index 00a37f2..a0efcfb 100644 --- a/mark_line.go +++ b/mark_line.go @@ -24,6 +24,7 @@ package charts import ( "github.com/golang/freetype/truetype" + "github.com/wcharczuk/go-chart/v2" ) func NewMarkLine(markLineTypes ...string) SeriesMarkLine { @@ -70,6 +71,10 @@ func (m *markLinePainter) Render() (Box, error) { if len(s.MarkLine.Data) == 0 { continue } + font := opt.Font + if font == nil { + font, _ = chart.GetDefaultFont() + } summary := s.Summary() for _, markLine := range s.MarkLine.Data { // 由于mark line会修改style,因此每次重新设置 @@ -82,7 +87,7 @@ func (m *markLinePainter) Render() (Box, error) { 2, }, }).OverrideTextStyle(Style{ - Font: opt.Font, + Font: font, FontColor: opt.FontColor, FontSize: labelFontSize, }) diff --git a/mark_line_test.go b/mark_line_test.go new file mode 100644 index 0000000..84152ce --- /dev/null +++ b/mark_line_test.go @@ -0,0 +1,89 @@ +// 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" + "github.com/wcharczuk/go-chart/v2/drawing" +) + +func TestMarkLine(t *testing.T) { + assert := assert.New(t) + + tests := []struct { + render func(*Painter) ([]byte, error) + result string + }{ + { + render: func(p *Painter) ([]byte, error) { + markLine := NewMarkLinePainter(p) + series := NewSeriesFromValues([]float64{ + 1, + 2, + 3, + }) + series.MarkLine = NewMarkLine( + SeriesMarkDataTypeMax, + SeriesMarkDataTypeAverage, + SeriesMarkDataTypeMin, + ) + markLine.Add(markLineRenderOption{ + FillColor: drawing.ColorBlack, + FontColor: drawing.ColorBlack, + StrokeColor: drawing.ColorBlack, + Series: series, + Range: NewRange(AxisRangeOption{ + Min: 0, + Max: 5, + Size: p.Height(), + DivideCount: 6, + }), + }) + _, err := markLine.Render() + if err != nil { + return nil, err + } + return p.Bytes() + }, + result: "\\n321", + }, + } + for _, tt := range tests { + p, err := NewPainter(PainterOptions{ + Type: ChartOutputSVG, + Width: 600, + Height: 400, + }, PainterThemeOption(defaultTheme)) + assert.Nil(err) + data, err := tt.render(p.Child(PainterPaddingOption(Box{ + Left: 20, + Top: 20, + Right: 20, + Bottom: 20, + }))) + assert.Nil(err) + assert.Equal(tt.result, string(data)) + } +} diff --git a/mark_point_test.go b/mark_point_test.go new file mode 100644 index 0000000..1a810cf --- /dev/null +++ b/mark_point_test.go @@ -0,0 +1,92 @@ +// 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" + "github.com/wcharczuk/go-chart/v2/drawing" +) + +func TestMarkPoint(t *testing.T) { + assert := assert.New(t) + + tests := []struct { + render func(*Painter) ([]byte, error) + result string + }{ + { + render: func(p *Painter) ([]byte, error) { + series := NewSeriesFromValues([]float64{ + 1, + 2, + 3, + }) + series.MarkPoint = NewMarkPoint(SeriesMarkDataTypeMax) + markPoint := NewMarkPointPainter(p) + markPoint.Add(markPointRenderOption{ + FillColor: drawing.ColorBlack, + Series: series, + Points: []Point{ + { + X: 10, + Y: 10, + }, + { + X: 30, + Y: 30, + }, + { + X: 50, + Y: 50, + }, + }, + }) + _, err := markPoint.Render() + if err != nil { + return nil, err + } + return p.Bytes() + }, + result: "\\n3", + }, + } + + for _, tt := range tests { + p, err := NewPainter(PainterOptions{ + Type: ChartOutputSVG, + Width: 600, + Height: 400, + }, PainterThemeOption(defaultTheme)) + assert.Nil(err) + data, err := tt.render(p.Child(PainterPaddingOption(Box{ + Left: 20, + Top: 20, + Right: 20, + Bottom: 20, + }))) + assert.Nil(err) + assert.Equal(tt.result, string(data)) + } +} diff --git a/pie_chart_test.go b/pie_chart_test.go new file mode 100644 index 0000000..c373a7e --- /dev/null +++ b/pie_chart_test.go @@ -0,0 +1,100 @@ +// 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 TestPieChart(t *testing.T) { + assert := assert.New(t) + + tests := []struct { + render func(*Painter) ([]byte, error) + result string + }{ + { + render: func(p *Painter) ([]byte, error) { + values := []float64{ + 1048, + 735, + 580, + 484, + 300, + } + _, err := NewPieChart(p, PieChartOption{ + SeriesList: NewPieSeriesList(values, PieSeriesOption{ + Label: SeriesLabel{ + Show: true, + }, + }), + Title: TitleOption{ + Text: "Rainfall vs Evaporation", + Subtext: "Fake Data", + Left: PositionCenter, + }, + Padding: Box{ + Top: 20, + Right: 20, + Bottom: 20, + Left: 20, + }, + Legend: LegendOption{ + Orient: OrientVertical, + Data: []string{ + "Search Engine", + "Direct", + "Email", + "Union Ads", + "Video Ads", + }, + Left: PositionLeft, + }, + }).Render() + if err != nil { + return nil, err + } + return p.Bytes() + }, + result: "\\nSearch EngineDirectEmailUnion AdsVideo AdsRainfall vs EvaporationFake DataSearch Engine: 33.3%Direct: 23.35%Email: 18.43%Union Ads: 15.37%Video Ads: 9.53%", + }, + } + for _, tt := range tests { + p, err := NewPainter(PainterOptions{ + Type: ChartOutputSVG, + Width: 600, + Height: 400, + }, PainterThemeOption(defaultTheme)) + assert.Nil(err) + data, err := tt.render(p.Child(PainterPaddingOption(Box{ + Left: 20, + Top: 20, + Right: 20, + Bottom: 20, + }))) + assert.Nil(err) + assert.Equal(tt.result, string(data)) + } +} diff --git a/radar_chart.go b/radar_chart.go index 610d5f7..5b8aa85 100644 --- a/radar_chart.go +++ b/radar_chart.go @@ -62,6 +62,20 @@ type RadarChartOption struct { backgroundIsFilled bool } +func NewRadarIndicators(names []string, values []float64) []RadarIndicator { + if len(names) != len(values) { + return nil + } + indicators := make([]RadarIndicator, len(names)) + for index, name := range names { + indicators[index] = RadarIndicator{ + Name: name, + Max: values[index], + } + } + return indicators +} + func NewRadarChart(p *Painter, opt RadarChartOption) *radarChart { if opt.Theme == nil { opt.Theme = defaultTheme diff --git a/radar_chart_test.go b/radar_chart_test.go new file mode 100644 index 0000000..baf616d --- /dev/null +++ b/radar_chart_test.go @@ -0,0 +1,109 @@ +// 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 ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRadarChart(t *testing.T) { + assert := assert.New(t) + + tests := []struct { + render func(*Painter) ([]byte, error) + result string + }{ + { + render: func(p *Painter) ([]byte, error) { + values := [][]float64{ + { + 4200, + 3000, + 20000, + 35000, + 50000, + 18000, + }, + { + 5000, + 14000, + 28000, + 26000, + 42000, + 21000, + }, + } + _, err := NewRadarChart(p, RadarChartOption{ + SeriesList: NewSeriesListDataFromValues(values, ChartTypeRadar), + Title: TitleOption{ + Text: "Basic Radar Chart", + }, + Legend: NewLegendOption([]string{ + "Allocated Budget", + "Actual Spending", + }), + RadarIndicators: NewRadarIndicators([]string{ + "Sales", + "Administration", + "Information Technology", + "Customer Support", + "Development", + "Marketing", + }, []float64{ + 6500, + 16000, + 30000, + 38000, + 52000, + 25000, + }), + }).Render() + if err != nil { + return nil, err + } + return p.Bytes() + }, + result: "\\nAllocated BudgetActual SpendingBasic Radar ChartSalesAdministrationInformation TechnologyCustomer SupportDevelopmentMarketing", + }, + } + for _, tt := range tests { + p, err := NewPainter(PainterOptions{ + Type: ChartOutputSVG, + Width: 600, + Height: 400, + }, PainterThemeOption(defaultTheme)) + assert.Nil(err) + data, err := tt.render(p.Child(PainterPaddingOption(Box{ + Left: 20, + Top: 20, + Right: 20, + Bottom: 20, + }))) + assert.Nil(err) + fmt.Println(string(data)) + assert.Equal(tt.result, string(data)) + } +} From 6695a3a062ed2091b706c2d206c55605a769610f Mon Sep 17 00:00:00 2001 From: vicanso Date: Tue, 21 Jun 2022 20:18:27 +0800 Subject: [PATCH 028/116] test: add test for charts --- chart_option.go | 6 +- chart_option_test.go | 451 +++++++++++++++++++++++++++++++++++++++++++ echarts.go | 4 +- echarts_test.go | 423 +++++++++++++++++++++++++++++++++++----- legend_test.go | 2 - radar_chart_test.go | 2 - 6 files changed, 827 insertions(+), 61 deletions(-) create mode 100644 chart_option_test.go diff --git a/chart_option.go b/chart_option.go index db2f57b..94574a1 100644 --- a/chart_option.go +++ b/chart_option.go @@ -69,9 +69,9 @@ type ChartOption struct { // OptionFunc option function type OptionFunc func(opt *ChartOption) -// PNGTypeOption set png type of chart's output -func PNGTypeOption() OptionFunc { - return TypeOptionFunc(ChartOutputPNG) +// SVGTypeOption set svg type of chart's output +func SVGTypeOption() OptionFunc { + return TypeOptionFunc(ChartOutputSVG) } // TypeOptionFunc set type of chart's output diff --git a/chart_option_test.go b/chart_option_test.go new file mode 100644 index 0000000..c77bb4f --- /dev/null +++ b/chart_option_test.go @@ -0,0 +1,451 @@ +// 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" + "github.com/wcharczuk/go-chart/v2/drawing" +) + +func TestChartOption(t *testing.T) { + assert := assert.New(t) + + fns := []OptionFunc{ + SVGTypeOption(), + FontFamilyOptionFunc("fontFamily"), + ThemeOptionFunc("theme"), + TitleTextOptionFunc("title"), + LegendLabelsOptionFunc([]string{ + "label", + }), + XAxisDataOptionFunc([]string{ + "xaxis", + }), + YAxisDataOptionFunc([]string{ + "yaxis", + }), + WidthOptionFunc(800), + HeightOptionFunc(600), + PaddingOptionFunc(Box{ + Left: 10, + Top: 10, + Right: 10, + Bottom: 10, + }), + BackgroundColorOptionFunc(drawing.ColorBlack), + } + opt := ChartOption{} + for _, fn := range fns { + fn(&opt) + } + assert.Equal(ChartOption{ + Type: ChartOutputSVG, + FontFamily: "fontFamily", + Theme: "theme", + Title: TitleOption{ + Text: "title", + }, + Legend: LegendOption{ + Data: []string{ + "label", + }, + }, + XAxis: XAxisOption{ + Data: []string{ + "xaxis", + }, + }, + YAxisOptions: []YAxisOption{ + { + Data: []string{ + "yaxis", + }, + }, + }, + Width: 800, + Height: 600, + Padding: Box{ + Left: 10, + Top: 10, + Right: 10, + Bottom: 10, + }, + BackgroundColor: drawing.ColorBlack, + }, opt) +} + +func TestChartOptionPieSeriesShowLabel(t *testing.T) { + assert := assert.New(t) + + opt := ChartOption{ + SeriesList: NewPieSeriesList([]float64{ + 1, + 2, + }), + } + PieSeriesShowLabel()(&opt) + assert.True(opt.SeriesList[0].Label.Show) +} + +func TestChartOptionMarkLine(t *testing.T) { + assert := assert.New(t) + opt := ChartOption{ + SeriesList: NewSeriesListDataFromValues([][]float64{ + {1, 2}, + }), + } + MarkLineOptionFunc(0, "min", "max")(&opt) + assert.Equal(NewMarkLine("min", "max"), opt.SeriesList[0].MarkLine) +} + +func TestChartOptionMarkPoint(t *testing.T) { + assert := assert.New(t) + opt := ChartOption{ + SeriesList: NewSeriesListDataFromValues([][]float64{ + {1, 2}, + }), + } + MarkPointOptionFunc(0, "min", "max")(&opt) + assert.Equal(NewMarkPoint("min", "max"), opt.SeriesList[0].MarkPoint) +} + +func TestLineRender(t *testing.T) { + assert := assert.New(t) + values := [][]float64{ + { + 120, + 132, + 101, + 134, + 90, + 230, + 210, + }, + { + 220, + 182, + 191, + 234, + 290, + 330, + 310, + }, + { + 150, + 232, + 201, + 154, + 190, + 330, + 410, + }, + { + 320, + 332, + 301, + 334, + 390, + 330, + 320, + }, + { + 820, + 932, + 901, + 934, + 1290, + 1330, + 1320, + }, + } + p, err := LineRender( + values, + SVGTypeOption(), + TitleTextOptionFunc("Line"), + XAxisDataOptionFunc([]string{ + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun", + }), + LegendLabelsOptionFunc([]string{ + "Email", + "Union Ads", + "Video Ads", + "Direct", + "Search Engine", + }, PositionCenter), + ) + assert.Nil(err) + data, err := p.Bytes() + assert.Nil(err) + assert.Equal("\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", string(data)) +} + +func TestBarRender(t *testing.T) { + assert := assert.New(t) + values := [][]float64{ + { + 2.0, + 4.9, + 7.0, + 23.2, + 25.6, + 76.7, + 135.6, + 162.2, + 32.6, + 20.0, + 6.4, + 3.3, + }, + { + 2.6, + 5.9, + 9.0, + 26.4, + 28.7, + 70.7, + 175.6, + 182.2, + 48.7, + 18.8, + 6.0, + 2.3, + }, + } + p, err := BarRender( + values, + SVGTypeOption(), + XAxisDataOptionFunc([]string{ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + }), + LegendLabelsOptionFunc([]string{ + "Rainfall", + "Evaporation", + }, PositionRight), + MarkLineOptionFunc(0, SeriesMarkDataTypeAverage), + MarkPointOptionFunc(0, SeriesMarkDataTypeMax, + SeriesMarkDataTypeMin), + // custom option func + func(opt *ChartOption) { + opt.SeriesList[1].MarkPoint = NewMarkPoint( + SeriesMarkDataTypeMax, + SeriesMarkDataTypeMin, + ) + opt.SeriesList[1].MarkLine = NewMarkLine( + SeriesMarkDataTypeAverage, + ) + }, + ) + assert.Nil(err) + data, err := p.Bytes() + assert.Nil(err) + assert.Equal("\\nRainfallEvaporation24020016012080400JanFebMarAprMayJunJulAugSepOctNovDec162.22182.22.341.6248.07", string(data)) +} + +func TestHorizontalBarRender(t *testing.T) { + assert := assert.New(t) + values := [][]float64{ + { + 18203, + 23489, + 29034, + 104970, + 131744, + 630230, + }, + { + 19325, + 23438, + 31000, + 121594, + 134141, + 681807, + }, + } + p, err := HorizontalBarRender( + values, + SVGTypeOption(), + TitleTextOptionFunc("World Population"), + PaddingOptionFunc(Box{ + Top: 20, + Right: 40, + Bottom: 20, + Left: 20, + }), + LegendLabelsOptionFunc([]string{ + "2011", + "2012", + }), + YAxisDataOptionFunc([]string{ + "Brazil", + "Indonesia", + "USA", + "India", + "China", + "World", + }), + ) + assert.Nil(err) + data, err := p.Bytes() + assert.Nil(err) + assert.Equal("\\n20112012World PopulationWorldChinaIndiaUSAIndonesiaBrazil0122.28k244.56k366.84k489.12k611.4k733.68k", string(data)) +} + +func TestPieRender(t *testing.T) { + assert := assert.New(t) + values := []float64{ + 1048, + 735, + 580, + 484, + 300, + } + p, err := PieRender( + values, + SVGTypeOption(), + TitleOptionFunc(TitleOption{ + Text: "Rainfall vs Evaporation", + Subtext: "Fake Data", + Left: PositionCenter, + }), + PaddingOptionFunc(Box{ + Top: 20, + Right: 20, + Bottom: 20, + Left: 20, + }), + LegendOptionFunc(LegendOption{ + Orient: OrientVertical, + Data: []string{ + "Search Engine", + "Direct", + "Email", + "Union Ads", + "Video Ads", + }, + Left: PositionLeft, + }), + PieSeriesShowLabel(), + ) + assert.Nil(err) + data, err := p.Bytes() + assert.Nil(err) + assert.Equal("\\nSearch EngineDirectEmailUnion AdsVideo AdsRainfall vs EvaporationFake DataSearch Engine: 33.3%Direct: 23.35%Email: 18.43%Union Ads: 15.37%Video Ads: 9.53%", string(data)) +} + +func TestRadarRender(t *testing.T) { + assert := assert.New(t) + + values := [][]float64{ + { + 4200, + 3000, + 20000, + 35000, + 50000, + 18000, + }, + { + 5000, + 14000, + 28000, + 26000, + 42000, + 21000, + }, + } + p, err := RadarRender( + values, + SVGTypeOption(), + TitleTextOptionFunc("Basic Radar Chart"), + LegendLabelsOptionFunc([]string{ + "Allocated Budget", + "Actual Spending", + }), + RadarIndicatorOptionFunc([]string{ + "Sales", + "Administration", + "Information Technology", + "Customer Support", + "Development", + "Marketing", + }, []float64{ + 6500, + 16000, + 30000, + 38000, + 52000, + 25000, + }), + ) + assert.Nil(err) + data, err := p.Bytes() + assert.Nil(err) + assert.Equal("\\nAllocated BudgetActual SpendingBasic Radar ChartSalesAdministrationInformation TechnologyCustomer SupportDevelopmentMarketing", string(data)) +} + +func TestFunnelRender(t *testing.T) { + assert := assert.New(t) + + values := []float64{ + 100, + 80, + 60, + 40, + 20, + } + p, err := FunnelRender( + values, + SVGTypeOption(), + TitleTextOptionFunc("Funnel"), + LegendLabelsOptionFunc([]string{ + "Show", + "Click", + "Visit", + "Inquiry", + "Order", + }), + ) + assert.Nil(err) + data, err := p.Bytes() + assert.Nil(err) + assert.Equal("\\nShowClickVisitInquiryOrderFunnelShow(100%)Click(80%)Visit(60%)Inquiry(40%)Order(20%)", string(data)) +} diff --git a/echarts.go b/echarts.go index d2602b3..fbe9a36 100644 --- a/echarts.go +++ b/echarts.go @@ -60,9 +60,9 @@ type EChartStyle struct { Color string `json:"color"` } -func (es *EChartStyle) ToStyle() chart.Style { +func (es *EChartStyle) ToStyle() Style { color := parseColor(es.Color) - return chart.Style{ + return Style{ FillColor: color, FontColor: color, StrokeColor: color, diff --git a/echarts_test.go b/echarts_test.go index 1ed14d3..9c31286 100644 --- a/echarts_test.go +++ b/echarts_test.go @@ -27,6 +27,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/wcharczuk/go-chart/v2/drawing" ) func TestConvertToArray(t *testing.T) { @@ -59,6 +60,7 @@ func TestEChartsSeriesDataValue(t *testing.T) { 2, }, }, es) + assert.Equal(NewEChartsSeriesDataValue(1, 2), es) assert.Equal(1.0, es.First()) } @@ -109,47 +111,380 @@ func TestEChartsXAxis(t *testing.T) { }, ex) } +func TestEChartStyle(t *testing.T) { + assert := assert.New(t) + + es := EChartStyle{ + Color: "#999", + } + color := drawing.Color{ + R: 153, + G: 153, + B: 153, + A: 255, + } + assert.Equal(Style{ + FillColor: color, + FontColor: color, + StrokeColor: color, + }, es.ToStyle()) +} + +func TestEChartsPadding(t *testing.T) { + assert := assert.New(t) + + eb := EChartsPadding{} + + err := eb.UnmarshalJSON([]byte(`1`)) + assert.Nil(err) + assert.Equal(Box{ + Left: 1, + Top: 1, + Right: 1, + Bottom: 1, + }, eb.Box) + + err = eb.UnmarshalJSON([]byte(`[2, 3]`)) + assert.Nil(err) + assert.Equal(Box{ + Left: 3, + Top: 2, + Right: 3, + Bottom: 2, + }, eb.Box) + + err = eb.UnmarshalJSON([]byte(`[4, 5, 6]`)) + assert.Nil(err) + assert.Equal(Box{ + Left: 5, + Top: 4, + Right: 5, + Bottom: 6, + }, eb.Box) + + err = eb.UnmarshalJSON([]byte(`[4, 5, 6, 7]`)) + assert.Nil(err) + assert.Equal(Box{ + Left: 7, + Top: 4, + Right: 5, + Bottom: 6, + }, eb.Box) +} + +func TestEChartsMarkPoint(t *testing.T) { + assert := assert.New(t) + + emp := EChartsMarkPoint{ + SymbolSize: 30, + Data: []EChartsMarkData{ + { + Type: "test", + }, + }, + } + assert.Equal(SeriesMarkPoint{ + SymbolSize: 30, + Data: []SeriesMarkData{ + { + Type: "test", + }, + }, + }, emp.ToSeriesMarkPoint()) +} + +func TestEChartsMarkLine(t *testing.T) { + assert := assert.New(t) + + eml := EChartsMarkLine{ + Data: []EChartsMarkData{ + { + Type: "min", + }, + { + Type: "max", + }, + }, + } + assert.Equal(SeriesMarkLine{ + Data: []SeriesMarkData{ + { + Type: "min", + }, + { + Type: "max", + }, + }, + }, eml.ToSeriesMarkLine()) +} + func TestEChartsOption(t *testing.T) { assert := assert.New(t) - opt := EChartsOption{} - err := json.Unmarshal([]byte(`{ + tests := []struct { + option string + }{ + { + option: `{ + "xAxis": { + "type": "category", + "data": [ + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun" + ] + }, + "yAxis": { + "type": "value" + }, + "series": [ + { + "data": [ + 120, + { + "value": 200, + "itemStyle": { + "color": "#a90000" + } + }, + 150, + 80, + 70, + 110, + 130 + ], + "type": "bar" + } + ] + }`, + }, + { + option: `{ + "title": { + "text": "Referer of a Website", + "subtext": "Fake Data", + "left": "center" + }, + "tooltip": { + "trigger": "item" + }, + "legend": { + "orient": "vertical", + "left": "left" + }, + "series": [ + { + "name": "Access From", + "type": "pie", + "radius": "50%", + "data": [ + { + "value": 1048, + "name": "Search Engine" + }, + { + "value": 735, + "name": "Direct" + }, + { + "value": 580, + "name": "Email" + }, + { + "value": 484, + "name": "Union Ads" + }, + { + "value": 300, + "name": "Video Ads" + } + ], + "emphasis": { + "itemStyle": { + "shadowBlur": 10, + "shadowOffsetX": 0, + "shadowColor": "rgba(0, 0, 0, 0.5)" + } + } + } + ] + }`, + }, + { + option: `{ + "title": { + "text": "Rainfall vs Evaporation", + "subtext": "Fake Data" + }, + "tooltip": { + "trigger": "axis" + }, + "legend": { + "data": [ + "Rainfall", + "Evaporation" + ] + }, + "toolbox": { + "show": true, + "feature": { + "dataView": { + "show": true, + "readOnly": false + }, + "magicType": { + "show": true, + "type": [ + "line", + "bar" + ] + }, + "restore": { + "show": true + }, + "saveAsImage": { + "show": true + } + } + }, + "calculable": true, + "xAxis": [ + { + "type": "category", + "data": [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec" + ] + } + ], + "yAxis": [ + { + "type": "value" + } + ], + "series": [ + { + "name": "Rainfall", + "type": "bar", + "data": [ + 2, + 4.9, + 7, + 23.2, + 25.6, + 76.7, + 135.6, + 162.2, + 32.6, + 20, + 6.4, + 3.3 + ], + "markPoint": { + "data": [ + { + "type": "max", + "name": "Max" + }, + { + "type": "min", + "name": "Min" + } + ] + }, + "markLine": { + "data": [ + { + "type": "average", + "name": "Avg" + } + ] + } + }, + { + "name": "Evaporation", + "type": "bar", + "data": [ + 2.6, + 5.9, + 9, + 26.4, + 28.7, + 70.7, + 175.6, + 182.2, + 48.7, + 18.8, + 6, + 2.3 + ], + "markPoint": { + "data": [ + { + "name": "Max", + "value": 182.2, + "xAxis": 7, + "yAxis": 183 + }, + { + "name": "Min", + "value": 2.3, + "xAxis": 11, + "yAxis": 3 + } + ] + }, + "markLine": { + "data": [ + { + "type": "average", + "name": "Avg" + } + ] + } + } + ] + }`, + }, + } + for _, tt := range tests { + opt := EChartsOption{} + err := json.Unmarshal([]byte(tt.option), &opt) + assert.Nil(err) + assert.NotEmpty(opt.Series) + assert.NotEmpty(opt.ToOption().SeriesList) + } +} + +func TestRenderEChartsToSVG(t *testing.T) { + assert := assert.New(t) + + data, err := RenderEChartsToSVG(`{ "title": { "text": "Rainfall vs Evaporation", "subtext": "Fake Data" }, - "tooltip": { - "trigger": "axis" - }, "legend": { "data": [ "Rainfall", "Evaporation" ] }, - "toolbox": { - "show": true, - "feature": { - "dataView": { - "show": true, - "readOnly": false - }, - "magicType": { - "show": true, - "type": [ - "line", - "bar" - ] - }, - "restore": { - "show": true - }, - "saveAsImage": { - "show": true - } - } - }, - "calculable": true, + "padding": [10, 30, 10, 10], "xAxis": [ { "type": "category", @@ -169,11 +504,6 @@ func TestEChartsOption(t *testing.T) { ] } ], - "yAxis": [ - { - "type": "value" - } - ], "series": [ { "name": "Rainfall", @@ -195,20 +525,17 @@ func TestEChartsOption(t *testing.T) { "markPoint": { "data": [ { - "type": "max", - "name": "Max" + "type": "max" }, { - "type": "min", - "name": "Min" + "type": "min" } ] }, "markLine": { "data": [ { - "type": "average", - "name": "Avg" + "type": "average" } ] } @@ -233,31 +560,23 @@ func TestEChartsOption(t *testing.T) { "markPoint": { "data": [ { - "name": "Max", - "value": 182.2, - "xAxis": 7, - "yAxis": 183 + "type": "max" }, { - "name": "Min", - "value": 2.3, - "xAxis": 11, - "yAxis": 3 + "type": "min" } ] }, "markLine": { "data": [ { - "type": "average", - "name": "Avg" + "type": "average" } ] } } ] - }`), &opt) - + }`) assert.Nil(err) - assert.NotEmpty(opt.Series) + assert.Equal("\\nRainfallEvaporationRainfall vs EvaporationFake Data24020016012080400JanFebMarAprMayJunJulAugSepOctNovDec162.22182.22.341.6248.07", string(data)) } diff --git a/legend_test.go b/legend_test.go index 9078006..526f178 100644 --- a/legend_test.go +++ b/legend_test.go @@ -23,7 +23,6 @@ package charts import ( - "fmt" "testing" "github.com/stretchr/testify/assert" @@ -98,7 +97,6 @@ func TestNewLegend(t *testing.T) { assert.Nil(err) data, err := tt.render(p) assert.Nil(err) - fmt.Println(string(data)) assert.Equal(tt.result, string(data)) } } diff --git a/radar_chart_test.go b/radar_chart_test.go index baf616d..79fd9ac 100644 --- a/radar_chart_test.go +++ b/radar_chart_test.go @@ -23,7 +23,6 @@ package charts import ( - "fmt" "testing" "github.com/stretchr/testify/assert" @@ -103,7 +102,6 @@ func TestRadarChart(t *testing.T) { Bottom: 20, }))) assert.Nil(err) - fmt.Println(string(data)) assert.Equal(tt.result, string(data)) } } From 4121829e6e9c483ac2c659b56a1762192a3f74ea Mon Sep 17 00:00:00 2001 From: vicanso Date: Tue, 21 Jun 2022 20:19:37 +0800 Subject: [PATCH 029/116] chore: add png type option function --- chart_option.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/chart_option.go b/chart_option.go index 94574a1..f2f3b51 100644 --- a/chart_option.go +++ b/chart_option.go @@ -74,6 +74,11 @@ func SVGTypeOption() OptionFunc { return TypeOptionFunc(ChartOutputSVG) } +// PNGTypeOption set png type of chart's output +func PNGTypeOption() OptionFunc { + return TypeOptionFunc(ChartOutputPNG) +} + // TypeOptionFunc set type of chart's output func TypeOptionFunc(t string) OptionFunc { return func(opt *ChartOption) { From 92458aece27cff52dc3f6b4a2de1df677cf13276 Mon Sep 17 00:00:00 2001 From: vicanso Date: Wed, 22 Jun 2022 20:30:10 +0800 Subject: [PATCH 030/116] refactor: adjust font size of mark point --- README.md | 2 +- README_zh.md | 2 +- chart_option_test.go | 2 +- charts.go | 1 + echarts_test.go | 2 +- mark_point.go | 25 +++++++++++++++++++------ mark_point_test.go | 2 +- 7 files changed, 25 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 8183871..d7d94f7 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [中文](./README_zh.md) -`go-charts` base on [go-chart](https://github.com/wcharczuk/go-chart),it is simpler way for generating charts, which supports `svg` and `png` format and themes: `light`, `dark`, `grafana` and `ant`. +`go-charts` base on [go-chart](https://github.com/wcharczuk/go-chart),it is simpler way for generating charts, which supports `svg` and `png` format and themes: `light`, `dark`, `grafana` and `ant`. The default format is `png` and the default theme is `light`. `Apache ECharts` is popular among Front-end developers, so `go-charts` supports the option of `Apache ECharts`. Developers can generate charts almost the same as `Apache ECharts`. diff --git a/README_zh.md b/README_zh.md index fed2d61..dbdaaf3 100644 --- a/README_zh.md +++ b/README_zh.md @@ -3,7 +3,7 @@ [![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/vicanso/go-charts/blob/master/LICENSE) [![Build Status](https://github.com/vicanso/go-charts/workflows/Test/badge.svg)](https://github.com/vicanso/go-charts/actions) -`go-charts`基于[go-chart](https://github.com/wcharczuk/go-chart),更简单方便的形式生成数据图表,支持`svg`与`png`两种方式的输出,支持主题`light`, `dark`, `grafana`以及`ant`。 +`go-charts`基于[go-chart](https://github.com/wcharczuk/go-chart),更简单方便的形式生成数据图表,支持`svg`与`png`两种方式的输出,支持主题`light`, `dark`, `grafana`以及`ant`。默认的输入格式为`png`,默认主题为`light`。 `Apache ECharts`在前端开发中得到众多开发者的认可,因此`go-charts`提供了兼容`Apache ECharts`的配置参数,简单快捷的生成相似的图表(`svg`或`png`),方便插入至Email或分享使用。下面为常用的图表截图(主题为light与grafana): diff --git a/chart_option_test.go b/chart_option_test.go index c77bb4f..5e53e46 100644 --- a/chart_option_test.go +++ b/chart_option_test.go @@ -277,7 +277,7 @@ func TestBarRender(t *testing.T) { assert.Nil(err) data, err := p.Bytes() assert.Nil(err) - assert.Equal("\\nRainfallEvaporation24020016012080400JanFebMarAprMayJunJulAugSepOctNovDec162.22182.22.341.6248.07", string(data)) + assert.Equal("\\nRainfallEvaporation24020016012080400JanFebMarAprMayJunJulAugSepOctNovDec162.22182.22.341.6248.07", string(data)) } func TestHorizontalBarRender(t *testing.T) { diff --git a/charts.go b/charts.go index cd1ac2b..a2e0ec0 100644 --- a/charts.go +++ b/charts.go @@ -28,6 +28,7 @@ import ( ) const labelFontSize = 10 +const smallLabelFontSize = 8 const defaultDotWidth = 2.0 const defaultStrokeWidth = 2.0 diff --git a/echarts_test.go b/echarts_test.go index 9c31286..4d50d9e 100644 --- a/echarts_test.go +++ b/echarts_test.go @@ -578,5 +578,5 @@ func TestRenderEChartsToSVG(t *testing.T) { ] }`) assert.Nil(err) - assert.Equal("\\nRainfallEvaporationRainfall vs EvaporationFake Data24020016012080400JanFebMarAprMayJunJulAugSepOctNovDec162.22182.22.341.6248.07", string(data)) + assert.Equal("\\nRainfallEvaporationRainfall vs EvaporationFake Data24020016012080400JanFebMarAprMayJunJulAugSepOctNovDec162.22182.22.341.6248.07", string(data)) } diff --git a/mark_point.go b/mark_point.go index 3d43a73..014b17f 100644 --- a/mark_point.go +++ b/mark_point.go @@ -24,6 +24,7 @@ package charts import ( "github.com/golang/freetype/truetype" + "github.com/wcharczuk/go-chart/v2/drawing" ) func NewMarkPoint(markPointTypes ...string) SeriesMarkPoint { @@ -63,7 +64,6 @@ func NewMarkPointPainter(p *Painter) *markPointPainter { func (m *markPointPainter) Render() (Box, error) { painter := m.p - theme := m.p.theme for _, opt := range m.options { s := opt.Series if len(s.MarkPoint.Data) == 0 { @@ -75,15 +75,23 @@ func (m *markPointPainter) Render() (Box, error) { if symbolSize == 0 { symbolSize = 30 } - painter.OverrideDrawingStyle(Style{ - FillColor: opt.FillColor, - }).OverrideTextStyle(Style{ - FontColor: theme.GetTextColor(), + textStyle := Style{ + FontColor: drawing.Color{ + R: 238, + G: 238, + B: 238, + A: 255, + }, FontSize: labelFontSize, StrokeWidth: 1, Font: opt.Font, - }) + } + painter.OverrideDrawingStyle(Style{ + FillColor: opt.FillColor, + }).OverrideTextStyle(textStyle) for _, markPointData := range s.MarkPoint.Data { + textStyle.FontSize = labelFontSize + painter.OverrideTextStyle(textStyle) p := points[summary.MinIndex] value := summary.MinValue switch markPointData.Type { @@ -95,6 +103,11 @@ func (m *markPointPainter) Render() (Box, error) { painter.Pin(p.X, p.Y-symbolSize>>1, symbolSize) text := commafWithDigits(value) textBox := painter.MeasureText(text) + if textBox.Width() > symbolSize { + textStyle.FontSize = smallLabelFontSize + painter.OverrideTextStyle(textStyle) + textBox = painter.MeasureText(text) + } painter.Text(text, p.X-textBox.Width()>>1, p.Y-symbolSize>>1-2) } } diff --git a/mark_point_test.go b/mark_point_test.go index 1a810cf..ffa01a7 100644 --- a/mark_point_test.go +++ b/mark_point_test.go @@ -69,7 +69,7 @@ func TestMarkPoint(t *testing.T) { } return p.Bytes() }, - result: "\\n3", + result: "\\n3", }, } From 706896737b5f46a3581b061cb6ca25169032d847 Mon Sep 17 00:00:00 2001 From: vicanso Date: Wed, 22 Jun 2022 21:04:16 +0800 Subject: [PATCH 031/116] docs: update documents --- bar_chart.go | 4 +++- charts.go | 3 +++ funnel_chart.go | 3 +++ grid.go | 23 ++++++++++++++++------- horizontal_bar_chart.go | 2 ++ legend.go | 4 ++++ line_chart.go | 2 ++ mark_line.go | 2 ++ mark_point.go | 2 ++ painter.go | 2 +- pie_chart.go | 2 ++ radar_chart.go | 3 +++ range.go | 18 ++++++++++++++---- series.go | 24 +++++++++++++++++++----- theme.go | 1 + title.go | 1 + xaxis.go | 2 ++ yaxis.go | 3 +++ 18 files changed, 83 insertions(+), 18 deletions(-) diff --git a/bar_chart.go b/bar_chart.go index 0ac9f47..26f8da5 100644 --- a/bar_chart.go +++ b/bar_chart.go @@ -32,6 +32,7 @@ type barChart struct { opt *BarChartOption } +// NewBarChart returns a bar chart renderer func NewBarChart(p *Painter, opt BarChartOption) *barChart { if opt.Theme == nil { opt.Theme = defaultTheme @@ -43,6 +44,7 @@ func NewBarChart(p *Painter, opt BarChartOption) *barChart { } type BarChartOption struct { + // The theme Theme ColorPalette // The font size Font *truetype.Font @@ -155,7 +157,7 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B if distance == 0 { distance = 5 } - text := NewValueLabelFormater(seriesNames, series.Label.Formatter)(index, item.Value, -1) + text := NewValueLabelFormatter(seriesNames, series.Label.Formatter)(index, item.Value, -1) labelStyle := Style{ FontColor: theme.GetTextColor(), FontSize: labelFontSize, diff --git a/charts.go b/charts.go index a2e0ec0..6c1c92b 100644 --- a/charts.go +++ b/charts.go @@ -35,11 +35,14 @@ const defaultStrokeWidth = 2.0 var defaultChartWidth = 600 var defaultChartHeight = 400 +// SetDefaultWidth sets default width of chart func SetDefaultWidth(width int) { if width > 0 { defaultChartWidth = width } } + +// SetDefaultHeight sets default height of chart func SetDefaultHeight(height int) { if height > 0 { defaultChartHeight = height diff --git a/funnel_chart.go b/funnel_chart.go index 7c04bfe..719853a 100644 --- a/funnel_chart.go +++ b/funnel_chart.go @@ -34,6 +34,7 @@ type funnelChart struct { opt *FunnelChartOption } +// NewFunnelSeriesList returns a series list for funnel func NewFunnelSeriesList(values []float64) SeriesList { seriesList := make(SeriesList, len(values)) for index, value := range values { @@ -44,6 +45,7 @@ func NewFunnelSeriesList(values []float64) SeriesList { return seriesList } +// NewFunnelChart returns a funnel chart renderer func NewFunnelChart(p *Painter, opt FunnelChartOption) *funnelChart { if opt.Theme == nil { opt.Theme = defaultTheme @@ -55,6 +57,7 @@ func NewFunnelChart(p *Painter, opt FunnelChartOption) *funnelChart { } type FunnelChartOption struct { + // The theme Theme ColorPalette // The font size Font *truetype.Font diff --git a/grid.go b/grid.go index 252fe2e..fb5dad6 100644 --- a/grid.go +++ b/grid.go @@ -28,16 +28,25 @@ type gridPainter struct { } type GridPainterOption struct { - StrokeWidth float64 - StrokeColor Color - Column int - Row int - IgnoreFirstRow bool - IgnoreLastRow bool + // The stroke width + StrokeWidth float64 + // The stroke color + StrokeColor Color + // The column of grid + Column int + // The row of grid + Row int + // Ignore first row + IgnoreFirstRow bool + // Ignore last row + IgnoreLastRow bool + // Ignore first column IgnoreFirstColumn bool - IgnoreLastColumn bool + // Ignore last column + IgnoreLastColumn bool } +// NewGridPainter returns new a grid renderer func NewGridPainter(p *Painter, opt GridPainterOption) *gridPainter { return &gridPainter{ p: p, diff --git a/horizontal_bar_chart.go b/horizontal_bar_chart.go index fb23734..30a9b7d 100644 --- a/horizontal_bar_chart.go +++ b/horizontal_bar_chart.go @@ -33,6 +33,7 @@ type horizontalBarChart struct { } type HorizontalBarChartOption struct { + // The theme Theme ColorPalette // The font size Font *truetype.Font @@ -50,6 +51,7 @@ type HorizontalBarChartOption struct { Legend LegendOption } +// NewHorizontalBarChart returns a horizontal bar chart renderer func NewHorizontalBarChart(p *Painter, opt HorizontalBarChartOption) *horizontalBarChart { if opt.Theme == nil { opt.Theme = defaultTheme diff --git a/legend.go b/legend.go index 65db102..2acd35b 100644 --- a/legend.go +++ b/legend.go @@ -36,6 +36,7 @@ const IconRect = "rect" const IconLineDot = "lineDot" type LegendOption struct { + // The theme Theme ColorPalette // Text array of legend Data []string @@ -60,6 +61,7 @@ type LegendOption struct { Show *bool } +// NewLegendOption returns a legend option func NewLegendOption(labels []string, left ...string) LegendOption { opt := LegendOption{ Data: labels, @@ -70,6 +72,7 @@ func NewLegendOption(labels []string, left ...string) LegendOption { return opt } +// IsEmpty checks legend is empty func (opt *LegendOption) IsEmpty() bool { isEmpty := true for _, v := range opt.Data { @@ -81,6 +84,7 @@ func (opt *LegendOption) IsEmpty() bool { return isEmpty } +// NewLegendPainter returns a legend renderer func NewLegendPainter(p *Painter, opt LegendOption) *legendPainter { return &legendPainter{ p: p, diff --git a/line_chart.go b/line_chart.go index f171813..0770447 100644 --- a/line_chart.go +++ b/line_chart.go @@ -32,6 +32,7 @@ type lineChart struct { opt *LineChartOption } +// NewLineChart returns a line chart render func NewLineChart(p *Painter, opt LineChartOption) *lineChart { if opt.Theme == nil { opt.Theme = defaultTheme @@ -43,6 +44,7 @@ func NewLineChart(p *Painter, opt LineChartOption) *lineChart { } type LineChartOption struct { + // The theme Theme ColorPalette // The font size Font *truetype.Font diff --git a/mark_line.go b/mark_line.go index a0efcfb..af1062d 100644 --- a/mark_line.go +++ b/mark_line.go @@ -27,6 +27,7 @@ import ( "github.com/wcharczuk/go-chart/v2" ) +// NewMarkLine returns a series mark line func NewMarkLine(markLineTypes ...string) SeriesMarkLine { data := make([]SeriesMarkData, len(markLineTypes)) for index, t := range markLineTypes { @@ -48,6 +49,7 @@ func (m *markLinePainter) Add(opt markLineRenderOption) { m.options = append(m.options, opt) } +// NewMarkLinePainter returns a mark line renderer func NewMarkLinePainter(p *Painter) *markLinePainter { return &markLinePainter{ p: p, diff --git a/mark_point.go b/mark_point.go index 014b17f..f6c93f3 100644 --- a/mark_point.go +++ b/mark_point.go @@ -27,6 +27,7 @@ import ( "github.com/wcharczuk/go-chart/v2/drawing" ) +// NewMarkPoint returns a series mark point func NewMarkPoint(markPointTypes ...string) SeriesMarkPoint { data := make([]SeriesMarkData, len(markPointTypes)) for index, t := range markPointTypes { @@ -55,6 +56,7 @@ type markPointRenderOption struct { Points []Point } +// NewMarkPointPainter returns a mark point renderer func NewMarkPointPainter(p *Painter) *markPointPainter { return &markPointPainter{ p: p, diff --git a/painter.go b/painter.go index da07007..c250369 100644 --- a/painter.go +++ b/painter.go @@ -136,7 +136,7 @@ func PainterWidthHeightOption(width, height int) PainterOption { } } -// NewPainter creates a new painter +// NewPainter creates a 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") diff --git a/pie_chart.go b/pie_chart.go index 972b4c1..6382140 100644 --- a/pie_chart.go +++ b/pie_chart.go @@ -36,6 +36,7 @@ type pieChart struct { } type PieChartOption struct { + // The theme Theme ColorPalette // The font size Font *truetype.Font @@ -51,6 +52,7 @@ type PieChartOption struct { backgroundIsFilled bool } +// NewPieChart returns a pie chart renderer func NewPieChart(p *Painter, opt PieChartOption) *pieChart { if opt.Theme == nil { opt.Theme = defaultTheme diff --git a/radar_chart.go b/radar_chart.go index 5b8aa85..eab70d5 100644 --- a/radar_chart.go +++ b/radar_chart.go @@ -45,6 +45,7 @@ type RadarIndicator struct { } type RadarChartOption struct { + // The theme Theme ColorPalette // The font size Font *truetype.Font @@ -62,6 +63,7 @@ type RadarChartOption struct { backgroundIsFilled bool } +// NewRadarIndicators returns a radar indicator list func NewRadarIndicators(names []string, values []float64) []RadarIndicator { if len(names) != len(values) { return nil @@ -76,6 +78,7 @@ func NewRadarIndicators(names []string, values []float64) []RadarIndicator { return indicators } +// NewRadarChart returns a radar chart renderer func NewRadarChart(p *Painter, opt RadarChartOption) *radarChart { if opt.Theme == nil { opt.Theme = defaultTheme diff --git a/range.go b/range.go index d5a9ef7..579a77f 100644 --- a/range.go +++ b/range.go @@ -37,13 +37,19 @@ type axisRange struct { } type AxisRangeOption struct { - Min float64 - Max float64 - Size int - Boundary bool + // The min value of axis + Min float64 + // The max value of axis + Max float64 + // The size of axis + Size int + // Boundary gap + Boundary bool + // The count of divide DivideCount int } +// NewRange returns a axis range func NewRange(opt AxisRangeOption) axisRange { max := opt.Max min := opt.Min @@ -88,6 +94,7 @@ func NewRange(opt AxisRangeOption) axisRange { } } +// Values returns values of range func (r axisRange) Values() []string { offset := (r.max - r.min) / float64(r.divideCount) values := make([]string, 0) @@ -108,10 +115,13 @@ func (r *axisRange) getRestHeight(value float64) int { return r.size - r.getHeight(value) } +// GetRange returns a range of index func (r *axisRange) GetRange(index int) (float64, float64) { unit := float64(r.size) / float64(r.divideCount) return unit * float64(index), unit * float64(index+1) } + +// AutoDivide divides the axis func (r *axisRange) AutoDivide() []int { return autoDivide(r.size, r.divideCount) } diff --git a/series.go b/series.go index 44c4749..87a719f 100644 --- a/series.go +++ b/series.go @@ -36,6 +36,7 @@ type SeriesData struct { Style Style } +// NewSeriesListDataFromValues returns a series list func NewSeriesListDataFromValues(values [][]float64, chartType ...string) SeriesList { seriesList := make(SeriesList, len(values)) for index, value := range values { @@ -44,6 +45,7 @@ func NewSeriesListDataFromValues(values [][]float64, chartType ...string) Series return seriesList } +// NewSeriesFromValues returns a series func NewSeriesFromValues(values []float64, chartType ...string) Series { s := Series{ Data: NewSeriesDataFromValues(values), @@ -54,6 +56,7 @@ func NewSeriesFromValues(values []float64, chartType ...string) Series { return s } +// NewSeriesDataFromValues return a series data func NewSeriesDataFromValues(values []float64) []SeriesData { data := make([]SeriesData, len(values)) for index, value := range values { @@ -204,13 +207,19 @@ func NewPieSeriesList(values []float64, opts ...PieSeriesOption) SeriesList { } type seriesSummary struct { - MaxIndex int - MaxValue float64 - MinIndex int - MinValue float64 + // The index of max value + MaxIndex int + // The max value + MaxValue float64 + // The index of min value + MinIndex int + // The min value + MinValue float64 + // THe average value AverageValue float64 } +// Summary get summary of series func (s *Series) Summary() seriesSummary { minIndex := -1 maxIndex := -1 @@ -237,6 +246,7 @@ func (s *Series) Summary() seriesSummary { } } +// Names returns the names of series list func (sl SeriesList) Names() []string { names := make([]string, len(sl)) for index, s := range sl { @@ -245,8 +255,10 @@ func (sl SeriesList) Names() []string { return names } +// LabelFormatter label formatter type LabelFormatter func(index int, value float64, percent float64) string +// NewPieLabelFormatter returns a pie label formatter func NewPieLabelFormatter(seriesNames []string, layout string) LabelFormatter { if len(layout) == 0 { layout = "{b}: {d}" @@ -254,13 +266,15 @@ func NewPieLabelFormatter(seriesNames []string, layout string) LabelFormatter { return NewLabelFormatter(seriesNames, layout) } -func NewValueLabelFormater(seriesNames []string, layout string) LabelFormatter { +// NewValueLabelFormatter returns a value formatter +func NewValueLabelFormatter(seriesNames []string, layout string) LabelFormatter { if len(layout) == 0 { layout = "{c}" } return NewLabelFormatter(seriesNames, layout) } +// NewLabelFormatter returns a label formaatter func NewLabelFormatter(seriesNames []string, layout string) LabelFormatter { return func(index int, value, percent float64) string { // 如果无percent的则设置为<0 diff --git a/theme.go b/theme.go index 26786b9..31c3bf8 100644 --- a/theme.go +++ b/theme.go @@ -220,6 +220,7 @@ func init() { SetDefaultTheme(ThemeLight) } +// SetDefaultTheme sets default theme func SetDefaultTheme(name string) { defaultTheme = NewTheme(name) } diff --git a/title.go b/title.go index a805c55..5af4c39 100644 --- a/title.go +++ b/title.go @@ -84,6 +84,7 @@ type titlePainter struct { opt *TitleOption } +// NewTitlePainter returns a title renderer func NewTitlePainter(p *Painter, opt TitleOption) *titlePainter { return &titlePainter{ p: p, diff --git a/xaxis.go b/xaxis.go index bfb57cb..00636a5 100644 --- a/xaxis.go +++ b/xaxis.go @@ -53,6 +53,7 @@ type XAxisOption struct { const defaultXAxisHeight = 30 +// NewXAxisOption returns a x axis option func NewXAxisOption(data []string, boundaryGap ...*bool) XAxisOption { opt := XAxisOption{ Data: data, @@ -89,6 +90,7 @@ func (opt *XAxisOption) ToAxisOption() AxisOption { return axisOpt } +// NewBottomXAxis returns a bottom x axis renderer func NewBottomXAxis(p *Painter, opt XAxisOption) *axisPainter { return NewAxisPainter(p, opt.ToAxisOption()) } diff --git a/yaxis.go b/yaxis.go index 265ac59..b0bfa86 100644 --- a/yaxis.go +++ b/yaxis.go @@ -50,6 +50,7 @@ type YAxisOption struct { isCategoryAxis bool } +// NewYAxisOptions returns a y axis option func NewYAxisOptions(data []string, others ...[]string) []YAxisOption { arr := [][]string{ data, @@ -95,6 +96,7 @@ func (opt *YAxisOption) ToAxisOption() AxisOption { return axisOpt } +// NewLeftYAxis returns a left y axis renderer func NewLeftYAxis(p *Painter, opt YAxisOption) *axisPainter { p = p.Child(PainterPaddingOption(Box{ Bottom: defaultXAxisHeight, @@ -102,6 +104,7 @@ func NewLeftYAxis(p *Painter, opt YAxisOption) *axisPainter { return NewAxisPainter(p, opt.ToAxisOption()) } +// NewRightYAxis returns a right y axis renderer func NewRightYAxis(p *Painter, opt YAxisOption) *axisPainter { p = p.Child(PainterPaddingOption(Box{ Bottom: defaultXAxisHeight, From 8c5647f65f160118c285a28b4e59a8ec6700ecc6 Mon Sep 17 00:00:00 2001 From: vicanso Date: Thu, 23 Jun 2022 20:32:25 +0800 Subject: [PATCH 032/116] test: add test for axis --- grid.go | 3 ++ grid_test.go | 18 ++++++++++ painter.go | 16 ++++++--- series_test.go | 89 +++++++++++++++++++++++++++++++++++++++++++++++ table.go | 53 ++++++++++++++++++++++++++++ title.go | 3 ++ title_test.go | 93 ++++++++++++++++++++++++++++++++++++++++++++++++++ util.go | 24 +++++++++++++ yaxis.go | 14 +++++--- yaxis_test.go | 70 +++++++++++++++++++++++++++++++++++++ 10 files changed, 374 insertions(+), 9 deletions(-) create mode 100644 series_test.go create mode 100644 table.go create mode 100644 title_test.go create mode 100644 yaxis_test.go diff --git a/grid.go b/grid.go index fb5dad6..0ebd226 100644 --- a/grid.go +++ b/grid.go @@ -32,6 +32,8 @@ type GridPainterOption struct { StrokeWidth float64 // The stroke color StrokeColor Color + // The spans of column + ColumnSpans []int // The column of grid Column int // The row of grid @@ -81,6 +83,7 @@ func (g *gridPainter) Render() (Box, error) { }) g.p.Grid(GridOption{ Column: opt.Column, + ColumnSpans: opt.ColumnSpans, Row: opt.Row, IgnoreColumnLines: ignoreColumnLines, IgnoreRowLines: ignoreRowLines, diff --git a/grid_test.go b/grid_test.go index f6880dc..3110a2b 100644 --- a/grid_test.go +++ b/grid_test.go @@ -54,6 +54,24 @@ func TestGrid(t *testing.T) { }, result: "\\n", }, + { + render: func(p *Painter) ([]byte, error) { + _, err := NewGridPainter(p, GridPainterOption{ + StrokeColor: drawing.ColorBlack, + ColumnSpans: []int{ + 2, + 5, + 3, + }, + Row: 6, + }).Render() + if err != nil { + return nil, err + } + return p.Bytes() + }, + result: "\\n", + }, } for _, tt := range tests { p, err := NewPainter(PainterOptions{ diff --git a/painter.go b/painter.go index c250369..1f9d418 100644 --- a/painter.go +++ b/painter.go @@ -69,8 +69,9 @@ type MultiTextOption struct { } type GridOption struct { - Column int - Row int + Column int + Row int + ColumnSpans []int // 忽略不展示的column IgnoreColumnLines []int // 忽略不展示的row @@ -542,6 +543,9 @@ func (p *Painter) TextFit(body string, x, y, width int) chart.Box { var output chart.Box for index, line := range lines { + if line == "" { + continue + } x0 := x y0 := y + output.Height() p.Text(line, x0, y0) @@ -690,8 +694,12 @@ func (p *Painter) Grid(opt GridOption) *Painter { }) } } - if opt.Column > 0 { - values := autoDivide(width, opt.Column) + columnCount := sumInt(opt.ColumnSpans) + if columnCount == 0 { + columnCount = opt.Column + } + if columnCount > 0 { + values := autoDivideSpans(width, columnCount, opt.ColumnSpans) drawLines(values, opt.IgnoreColumnLines, true) } if opt.Row > 0 { diff --git a/series_test.go b/series_test.go new file mode 100644 index 0000000..40d2f91 --- /dev/null +++ b/series_test.go @@ -0,0 +1,89 @@ +// 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 TestNewSeriesListDataFromValues(t *testing.T) { + assert := assert.New(t) + + assert.Equal(SeriesList{ + { + Type: ChartTypeBar, + Data: []SeriesData{ + { + Value: 1.0, + }, + }, + }, + }, NewSeriesListDataFromValues([][]float64{ + { + 1, + }, + }, ChartTypeBar)) +} + +func TestSeriesLists(t *testing.T) { + assert := assert.New(t) + seriesList := NewSeriesListDataFromValues([][]float64{ + { + 1, + 2, + }, + { + 10, + }, + }, ChartTypeBar) + + assert.Equal(2, len(seriesList.Filter(ChartTypeBar))) + assert.Equal(0, len(seriesList.Filter(ChartTypeLine))) + + max, min := seriesList.GetMaxMin(0) + assert.Equal(float64(10), max) + assert.Equal(float64(1), min) + + assert.Equal(seriesSummary{ + MaxIndex: 1, + MaxValue: 2, + MinIndex: 0, + MinValue: 1, + AverageValue: 1.5, + }, seriesList[0].Summary()) +} + +func TestFormatter(t *testing.T) { + assert := assert.New(t) + + assert.Equal("a: 12%", NewPieLabelFormatter([]string{ + "a", + "b", + }, "")(0, 10, 0.12)) + + assert.Equal("10", NewValueLabelFormatter([]string{ + "a", + "b", + }, "")(0, 10, 0.12)) +} diff --git a/table.go b/table.go new file mode 100644 index 0000000..e47914c --- /dev/null +++ b/table.go @@ -0,0 +1,53 @@ +// 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 + +type tableChart struct { + p *Painter + opt *TableChartOption +} + +func NewTableChart(p *Painter, opt TableChartOption) *tableChart { + if opt.Theme == nil { + opt.Theme = defaultTheme + } + return &tableChart{ + p: p, + opt: &opt, + } +} + +type TableChartOption struct { + // The theme + Theme ColorPalette + // The padding of table cell + Padding Box + // The header data of table + HeaderData []string + // The data of table + Data [][]string +} + +func (c *tableChart) Render() (Box, error) { + return BoxZero, nil +} diff --git a/title.go b/title.go index 5af4c39..5cdd161 100644 --- a/title.go +++ b/title.go @@ -97,6 +97,9 @@ func (t *titlePainter) Render() (Box, error) { p := t.p theme := opt.Theme + if theme == nil { + theme = p.theme + } if opt.Text == "" && opt.Subtext == "" { return BoxZero, nil } diff --git a/title_test.go b/title_test.go new file mode 100644 index 0000000..add8163 --- /dev/null +++ b/title_test.go @@ -0,0 +1,93 @@ +// 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 TestTitleRenderer(t *testing.T) { + assert := assert.New(t) + tests := []struct { + render func(*Painter) ([]byte, error) + result string + }{ + { + render: func(p *Painter) ([]byte, error) { + _, err := NewTitlePainter(p, TitleOption{ + Text: "title", + Subtext: "subTitle", + Left: "20", + Top: "20", + }).Render() + if err != nil { + return nil, err + } + return p.Bytes() + }, + result: "\\ntitlesubTitle", + }, + { + render: func(p *Painter) ([]byte, error) { + _, err := NewTitlePainter(p, TitleOption{ + Text: "title", + Subtext: "subTitle", + Left: "20%", + Top: "20", + }).Render() + if err != nil { + return nil, err + } + return p.Bytes() + }, + result: "\\ntitlesubTitle", + }, + { + render: func(p *Painter) ([]byte, error) { + _, err := NewTitlePainter(p, TitleOption{ + Text: "title", + Subtext: "subTitle", + Left: PositionRight, + }).Render() + if err != nil { + return nil, err + } + return p.Bytes() + }, + result: "\\ntitlesubTitle", + }, + } + for _, tt := range tests { + p, err := NewPainter(PainterOptions{ + Type: ChartOutputSVG, + Width: 600, + Height: 400, + }, PainterThemeOption(defaultTheme)) + assert.Nil(err) + data, err := tt.render(p) + assert.Nil(err) + assert.Equal(tt.result, string(data)) + } +} diff --git a/util.go b/util.go index adfa9fd..a33c6d2 100644 --- a/util.go +++ b/util.go @@ -90,6 +90,30 @@ func autoDivide(max, size int) []int { return values } +func autoDivideSpans(max, size int, spans []int) []int { + values := autoDivide(max, size) + // 重新合并 + if len(spans) != 0 { + newValues := make([]int, len(spans)+1) + newValues[0] = 0 + end := 0 + for index, v := range spans { + end += v + newValues[index+1] = values[end] + } + values = newValues + } + return values +} + +func sumInt(values []int) int { + sum := 0 + for _, v := range values { + sum += v + } + return sum +} + // measureTextMaxWidthHeight returns maxWidth and maxHeight of text list func measureTextMaxWidthHeight(textList []string, p *Painter) (int, int) { maxWidth := 0 diff --git a/yaxis.go b/yaxis.go index b0bfa86..eb9034c 100644 --- a/yaxis.go +++ b/yaxis.go @@ -65,14 +65,18 @@ func NewYAxisOptions(data []string, others ...[]string) []YAxisOption { return opts } -func (opt *YAxisOption) ToAxisOption() AxisOption { +func (opt *YAxisOption) ToAxisOption(p *Painter) AxisOption { position := PositionLeft if opt.Position == PositionRight { position = PositionRight } + theme := opt.Theme + if theme == nil { + theme = p.theme + } axisOpt := AxisOption{ Formatter: opt.Formatter, - Theme: opt.Theme, + Theme: theme, Data: opt.Data, Position: position, FontSize: opt.FontSize, @@ -81,7 +85,7 @@ func (opt *YAxisOption) ToAxisOption() AxisOption { FontColor: opt.FontColor, BoundaryGap: FalseFlag(), SplitLineShow: true, - SplitLineColor: opt.Theme.GetAxisSplitLineColor(), + SplitLineColor: theme.GetAxisSplitLineColor(), Show: opt.Show, } if !opt.Color.IsZero() { @@ -101,7 +105,7 @@ func NewLeftYAxis(p *Painter, opt YAxisOption) *axisPainter { p = p.Child(PainterPaddingOption(Box{ Bottom: defaultXAxisHeight, })) - return NewAxisPainter(p, opt.ToAxisOption()) + return NewAxisPainter(p, opt.ToAxisOption(p)) } // NewRightYAxis returns a right y axis renderer @@ -109,7 +113,7 @@ func NewRightYAxis(p *Painter, opt YAxisOption) *axisPainter { p = p.Child(PainterPaddingOption(Box{ Bottom: defaultXAxisHeight, })) - axisOpt := opt.ToAxisOption() + axisOpt := opt.ToAxisOption(p) axisOpt.Position = PositionRight axisOpt.SplitLineShow = false return NewAxisPainter(p, axisOpt) diff --git a/yaxis_test.go b/yaxis_test.go new file mode 100644 index 0000000..0f565ac --- /dev/null +++ b/yaxis_test.go @@ -0,0 +1,70 @@ +// 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 TestRightYAxis(t *testing.T) { + assert := assert.New(t) + tests := []struct { + render func(*Painter) ([]byte, error) + result string + }{ + { + render: func(p *Painter) ([]byte, error) { + opt := NewYAxisOptions([]string{ + "a", + "b", + "c", + "d", + })[0] + _, err := NewRightYAxis(p, opt).Render() + if err != nil { + return nil, err + } + return p.Bytes() + }, + result: "\\nabcd", + }, + } + for _, tt := range tests { + p, err := NewPainter(PainterOptions{ + Type: ChartOutputSVG, + Width: 600, + Height: 400, + }, PainterThemeOption(defaultTheme), PainterPaddingOption(Box{ + Top: 10, + Right: 10, + Bottom: 10, + Left: 10, + })) + assert.Nil(err) + data, err := tt.render(p) + assert.Nil(err) + assert.Equal(tt.result, string(data)) + } +} From 2fb0ebcbf7a5f8efd8c4f14c1b3056e2e29af688 Mon Sep 17 00:00:00 2001 From: vicanso Date: Thu, 23 Jun 2022 23:29:13 +0800 Subject: [PATCH 033/116] feat: support table render --- chart_option.go | 69 ++++++++++++++ examples/table/main.go | 79 ++++++++++++++++ painter.go | 22 +++-- painter_test.go | 28 +++--- table.go | 201 ++++++++++++++++++++++++++++++++++++++++- table_test.go | 100 ++++++++++++++++++++ 6 files changed, 476 insertions(+), 23 deletions(-) create mode 100644 examples/table/main.go create mode 100644 table_test.go diff --git a/chart_option.go b/chart_option.go index f2f3b51..b7b4714 100644 --- a/chart_option.go +++ b/chart_option.go @@ -23,6 +23,7 @@ package charts import ( + "fmt" "sort" "github.com/golang/freetype/truetype" @@ -336,3 +337,71 @@ func FunnelRender(values []float64, opts ...OptionFunc) (*Painter, error) { SeriesList: seriesList, }, opts...) } + +func TableRender(opt TableChartOption) (*Painter, error) { + if opt.Type == "" { + opt.Type = ChartOutputPNG + } + if opt.Width <= 0 { + opt.Width = defaultChartWidth + } + if opt.Height <= 0 { + opt.Height = defaultChartHeight + } + if opt.Font == nil { + opt.Font, _ = chart.GetDefaultFont() + } + + p, err := NewPainter(PainterOptions{ + Type: opt.Type, + Width: opt.Width, + Height: opt.Height, + Font: opt.Font, + }) + if err != nil { + return nil, err + } + info, err := NewTableChart(p, opt).render() + if err != nil { + return nil, err + } + fmt.Println(*info) + fmt.Println(info.Height) + + p, err = NewPainter(PainterOptions{ + Type: opt.Type, + Width: info.Width, + Height: info.Height, + Font: opt.Font, + }) + if err != nil { + return nil, err + } + _, err = NewTableChart(p, opt).Render() + if err != nil { + return nil, err + } + + // opt := ChartOption{} + // for _, fn := range opts { + // fn(&opt) + // } + // opt.fillDefault() + // p, err := NewPainter(PainterOptions{ + // Type: opt.Type, + // Width: opt.Width, + // Height: opt.Height, + // Font: opt.font, + // }) + // if err != nil { + // return nil, err + // } + // _, err = NewTableChart(p, TableChartOption{ + // Header: header, + // Data: data, + // }).Render() + // if err != nil { + // return nil, err + // } + return p, nil +} diff --git a/examples/table/main.go b/examples/table/main.go new file mode 100644 index 0000000..650566c --- /dev/null +++ b/examples/table/main.go @@ -0,0 +1,79 @@ +package main + +import ( + "io/ioutil" + "os" + "path/filepath" + + "github.com/vicanso/go-charts/v2" +) + +func writeFile(buf []byte) error { + tmpPath := "./tmp" + err := os.MkdirAll(tmpPath, 0700) + if err != nil { + return err + } + + file := filepath.Join(tmpPath, "table.png") + err = ioutil.WriteFile(file, buf, 0600) + if err != nil { + return err + } + return nil +} + +func main() { + p, err := charts.TableRender(charts.TableChartOption{ + Header: []string{ + "Name", + "Age", + "Address", + "Tag", + "Action", + }, + Spans: []int{ + 1, + 1, + 2, + 1, + 1, + }, + Data: [][]string{ + { + "John Brown", + "32", + "New York No. 1 Lake Park", + "nice, developer", + "Send Mail", + }, + { + "Jim Green ", + "42", + "London No. 1 Lake Park", + "wow", + "Send Mail", + }, + { + "Joe Black ", + "32", + "Sidney No. 1 Lake Park", + "cool, teacher", + "Send Mail", + }, + }, + }, + ) + if err != nil { + panic(err) + } + + buf, err := p.Bytes() + if err != nil { + panic(err) + } + err = writeFile(buf) + if err != nil { + panic(err) + } +} diff --git a/painter.go b/painter.go index 1f9d418..c787315 100644 --- a/painter.go +++ b/painter.go @@ -468,7 +468,7 @@ func (p *Painter) SmoothLineStroke(points []Point) *Painter { return p } -func (p *Painter) SetBackground(width, height int, color Color) *Painter { +func (p *Painter) SetBackground(width, height int, color Color, inside ...bool) *Painter { r := p.render s := chart.Style{ FillColor: color, @@ -476,12 +476,20 @@ func (p *Painter) SetBackground(width, height int, color Color) *Painter { // 背景色 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) + if len(inside) != 0 && inside[0] { + p.MoveTo(0, 0) + p.LineTo(width, 0) + p.LineTo(width, height) + p.LineTo(0, height) + p.LineTo(0, 0) + } else { + // 设置背景色不使用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 } diff --git a/painter_test.go b/painter_test.go index 8892563..96e41ef 100644 --- a/painter_test.go +++ b/painter_test.go @@ -143,13 +143,13 @@ func TestPainter(t *testing.T) { fn: func(p *Painter) { p.SetStyle(Style{ StrokeWidth: 1, - StrokeColor: drawing.Color{ + StrokeColor: Color{ R: 84, G: 112, B: 198, A: 255, }, - FillColor: drawing.Color{ + FillColor: Color{ R: 84, G: 112, B: 198, @@ -165,13 +165,13 @@ func TestPainter(t *testing.T) { fn: func(p *Painter) { p.SetStyle(Style{ StrokeWidth: 1, - StrokeColor: drawing.Color{ + StrokeColor: Color{ R: 84, G: 112, B: 198, A: 255, }, - FillColor: drawing.Color{ + FillColor: Color{ R: 84, G: 112, B: 198, @@ -187,13 +187,13 @@ func TestPainter(t *testing.T) { fn: func(p *Painter) { p.SetStyle(Style{ StrokeWidth: 1, - StrokeColor: drawing.Color{ + StrokeColor: Color{ R: 84, G: 112, B: 198, A: 255, }, - FillColor: drawing.Color{ + FillColor: Color{ R: 84, G: 112, B: 198, @@ -209,13 +209,13 @@ func TestPainter(t *testing.T) { fn: func(p *Painter) { p.SetStyle(Style{ StrokeWidth: 1, - StrokeColor: drawing.Color{ + StrokeColor: Color{ R: 84, G: 112, B: 198, A: 255, }, - FillColor: drawing.Color{ + FillColor: Color{ R: 84, G: 112, B: 198, @@ -231,13 +231,13 @@ func TestPainter(t *testing.T) { fn: func(p *Painter) { p.SetStyle(Style{ StrokeWidth: 1, - StrokeColor: drawing.Color{ + StrokeColor: Color{ R: 84, G: 112, B: 198, A: 255, }, - FillColor: drawing.Color{ + FillColor: Color{ R: 84, G: 112, B: 198, @@ -253,13 +253,13 @@ func TestPainter(t *testing.T) { fn: func(p *Painter) { p.SetStyle(Style{ StrokeWidth: 1, - StrokeColor: drawing.Color{ + StrokeColor: Color{ R: 84, G: 112, B: 198, A: 255, }, - FillColor: drawing.Color{ + FillColor: Color{ R: 84, G: 112, B: 198, @@ -279,7 +279,7 @@ func TestPainter(t *testing.T) { fn: func(p *Painter) { p.SetStyle(Style{ StrokeWidth: 1, - StrokeColor: drawing.Color{ + StrokeColor: Color{ R: 84, G: 112, B: 198, @@ -297,7 +297,7 @@ func TestPainter(t *testing.T) { { fn: func(p *Painter) { p.SetDrawingStyle(Style{ - FillColor: drawing.Color{ + FillColor: Color{ R: 84, G: 112, B: 198, diff --git a/table.go b/table.go index e47914c..34dee67 100644 --- a/table.go +++ b/table.go @@ -22,6 +22,14 @@ package charts +import ( + "errors" + + "github.com/golang/freetype/truetype" + "github.com/wcharczuk/go-chart/v2" + "github.com/wcharczuk/go-chart/v2/drawing" +) + type tableChart struct { p *Painter opt *TableChartOption @@ -38,16 +46,205 @@ func NewTableChart(p *Painter, opt TableChartOption) *tableChart { } type TableChartOption struct { + // The output type + Type string + // The width of table + Width int + // The height of table + Height int // The theme Theme ColorPalette // The padding of table cell Padding Box // The header data of table - HeaderData []string + Header []string // The data of table Data [][]string + // The span of table column + Spans []int + // The font size of table + FontSize float64 + Font *truetype.Font + // The font color of table + FontColor Color + // The background color of header + HeaderBackgroundColor Color + // The header font color + HeaderFontColor Color + // The background color of row + RowBackgroundColors []Color + // The background color + BackgroundColor Color +} + +var defaultTableHeaderColor = Color{ + R: 34, + G: 34, + B: 34, + A: 255, +} +var defaultTableRowColors = []Color{ + drawing.ColorWhite, + { + R: 242, + G: 242, + B: 242, + A: 255, + }, +} +var defaultTablePadding = Box{ + Left: 10, + Top: 10, + Right: 10, + Bottom: 10, +} + +type renderInfo struct { + Width int + Height int + HeaderHeight int + RowHeights []int +} + +func (c *tableChart) render() (*renderInfo, error) { + info := renderInfo{ + RowHeights: make([]int, 0), + } + p := c.p + opt := c.opt + if len(opt.Header) == 0 { + return nil, errors.New("header can not be nil") + } + theme := opt.Theme + if theme == nil { + theme = p.theme + } + fontSize := opt.FontSize + if fontSize == 0 { + fontSize = 12 + } + fontColor := opt.FontColor + if fontColor.IsZero() { + fontColor = theme.GetTextColor() + } + font := opt.Font + if font == nil { + font = theme.GetFont() + } + headerFontColor := opt.HeaderFontColor + if opt.HeaderFontColor.IsZero() { + headerFontColor = drawing.ColorWhite + } + + spans := opt.Spans + if len(spans) != 0 && len(spans) != len(opt.Header) { + newSpans := make([]int, len(opt.Header)) + for index := range opt.Header { + if len(spans) < index { + newSpans[index] = 1 + } else { + newSpans[index] = spans[index] + } + } + spans = newSpans + } + + values := autoDivideSpans(p.Width(), len(opt.Header)+1, spans) + height := 0 + textStyle := Style{ + FontSize: fontSize, + FontColor: headerFontColor, + FillColor: headerFontColor, + Font: font, + } + p.SetStyle(textStyle) + + headerHeight := 0 + padding := opt.Padding + if padding.IsZero() { + padding = defaultTablePadding + } + + renderTableCells := func(textList []string, currentHeight int, cellPadding Box) int { + cellMaxHeight := 0 + paddingHeight := cellPadding.Top + cellPadding.Bottom + paddingWidth := cellPadding.Left + cellPadding.Right + for index, text := range textList { + x := values[index] + y := currentHeight + cellPadding.Top + width := values[index+1] - x + x += cellPadding.Left + width -= paddingWidth + box := p.TextFit(text, x, y+int(fontSize), width) + if box.Height()+paddingHeight > cellMaxHeight { + cellMaxHeight = box.Height() + paddingHeight + } + } + return cellMaxHeight + } + + headerHeight = renderTableCells(opt.Header, height, padding) + height += headerHeight + info.HeaderHeight = headerHeight + + textStyle.FontColor = fontColor + textStyle.FillColor = fontColor + p.SetStyle(textStyle) + for _, textList := range opt.Data { + cellHeight := renderTableCells(textList, height, padding) + info.RowHeights = append(info.RowHeights, cellHeight) + height += cellHeight + } + + info.Width = p.Width() + info.Height = height + return &info, nil } func (c *tableChart) Render() (Box, error) { - return BoxZero, nil + p := c.p + opt := c.opt + if !opt.BackgroundColor.IsZero() { + p.SetBackground(p.Width(), p.Height(), opt.BackgroundColor) + } + headerBGColor := opt.HeaderBackgroundColor + if headerBGColor.IsZero() { + headerBGColor = defaultTableHeaderColor + } + + r := p.render + newRender, err := chart.SVG(p.Width(), 100) + if err != nil { + return BoxZero, err + } + p.render = newRender + info, err := c.render() + if err != nil { + return BoxZero, err + } + p.render = r + // 如果设置表头背景色 + p.SetBackground(info.Width, info.HeaderHeight, headerBGColor, true) + currentHeight := info.HeaderHeight + rowColors := opt.RowBackgroundColors + if len(rowColors) == 0 { + rowColors = defaultTableRowColors + } + for index, h := range info.RowHeights { + color := rowColors[index%len(rowColors)] + child := p.Child(PainterPaddingOption(Box{ + Top: currentHeight, + })) + child.SetBackground(p.Width(), h, color, true) + currentHeight += h + } + _, err = c.render() + if err != nil { + return BoxZero, err + } + + return Box{ + Right: info.Width, + Bottom: info.Height, + }, nil } diff --git a/table_test.go b/table_test.go new file mode 100644 index 0000000..c54de25 --- /dev/null +++ b/table_test.go @@ -0,0 +1,100 @@ +// 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 ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestTableChart(t *testing.T) { + assert := assert.New(t) + + tests := []struct { + render func(*Painter) ([]byte, error) + result string + }{ + { + render: func(p *Painter) ([]byte, error) { + _, err := NewTableChart(p, TableChartOption{ + Header: []string{ + "Name", + "Age", + "Address", + "Tag", + "Action", + }, + Spans: []int{ + 1, + 1, + 2, + 1, + 1, + }, + Data: [][]string{ + { + "John Brown", + "32", + "New York No. 1 Lake Park", + "nice, developer", + "Send Mail", + }, + { + "Jim Green ", + "42", + "London No. 1 Lake Park", + "wow", + "Send Mail", + }, + { + "Joe Black ", + "32", + "Sidney No. 1 Lake Park", + "cool, teacher", + "Send Mail", + }, + }, + }).Render() + if err != nil { + return nil, err + } + return p.Bytes() + }, + }, + } + for _, tt := range tests { + p, err := NewPainter(PainterOptions{ + Type: ChartOutputSVG, + Width: 600, + Height: 400, + }, PainterThemeOption(defaultTheme)) + assert.Nil(err) + data, err := tt.render(p) + assert.Nil(err) + fmt.Println(string(data)) + assert.Equal(tt.result, string(data)) + } + +} From b3a3018ea2eb6f71b126ce053f1d9410225a0689 Mon Sep 17 00:00:00 2001 From: vicanso Date: Sat, 25 Jun 2022 08:21:27 +0800 Subject: [PATCH 034/116] feat: support table redner --- README.md | 68 +++++++++++++++ README_zh.md | 64 ++++++++++++++ chart_option.go | 61 ++++++------- examples/charts/main.go | 42 +++++++++ examples/table/main.go | 76 ++++++++-------- painter.go | 4 + table.go | 188 ++++++++++++++++++++++++++++++---------- table_test.go | 46 +++++++++- 8 files changed, 433 insertions(+), 116 deletions(-) diff --git a/README.md b/README.md index d7d94f7..d0549f0 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,9 @@ Screenshot of common charts, the left part is light theme, the right part is gra go-charts

+

+ go-table +

## Chart Type These chart types are supported: `line`, `bar`, `pie`, `radar` or `funnel`. @@ -374,6 +377,71 @@ func main() { } ``` +### Table + +```go +package main + +import ( + "github.com/vicanso/go-charts/v2" +) + +func main() { + header := []string{ + "Name", + "Age", + "Address", + "Tag", + "Action", + } + data := [][]string{ + { + "John Brown", + "32", + "New York No. 1 Lake Park", + "nice, developer", + "Send Mail", + }, + { + "Jim Green ", + "42", + "London No. 1 Lake Park", + "wow", + "Send Mail", + }, + { + "Joe Black ", + "32", + "Sidney No. 1 Lake Park", + "cool, teacher", + "Send Mail", + }, + } + spans := map[int]int{ + 0: 2, + 1: 1, + // 设置第三列的span + 2: 3, + 3: 2, + 4: 2, + } + p, err := charts.TableRender( + header, + data, + spans, + ) + if err != nil { + panic(err) + } + + buf, err := p.Bytes() + if err != nil { + panic(err) + } + // snip... +} +``` + ### ECharts Render ```go diff --git a/README_zh.md b/README_zh.md index dbdaaf3..487a365 100644 --- a/README_zh.md +++ b/README_zh.md @@ -373,6 +373,70 @@ func main() { } ``` +### Table + +```go +package main + +import ( + "github.com/vicanso/go-charts/v2" +) + +func main() { + header := []string{ + "Name", + "Age", + "Address", + "Tag", + "Action", + } + data := [][]string{ + { + "John Brown", + "32", + "New York No. 1 Lake Park", + "nice, developer", + "Send Mail", + }, + { + "Jim Green ", + "42", + "London No. 1 Lake Park", + "wow", + "Send Mail", + }, + { + "Joe Black ", + "32", + "Sidney No. 1 Lake Park", + "cool, teacher", + "Send Mail", + }, + } + spans := map[int]int{ + 0: 2, + 1: 1, + // 设置第三列的span + 2: 3, + 3: 2, + 4: 2, + } + p, err := charts.TableRender( + header, + data, + spans, + ) + if err != nil { + panic(err) + } + + buf, err := p.Bytes() + if err != nil { + panic(err) + } + // snip... +} +``` ### ECharts Render ```go diff --git a/chart_option.go b/chart_option.go index b7b4714..39de686 100644 --- a/chart_option.go +++ b/chart_option.go @@ -23,7 +23,6 @@ package charts import ( - "fmt" "sort" "github.com/golang/freetype/truetype" @@ -338,24 +337,44 @@ func FunnelRender(values []float64, opts ...OptionFunc) (*Painter, error) { }, opts...) } -func TableRender(opt TableChartOption) (*Painter, error) { +// TableRender table chart render +func TableRender(header []string, data [][]string, spanMaps ...map[int]int) (*Painter, error) { + opt := TableChartOption{ + Header: header, + Data: data, + } + if len(spanMaps) != 0 { + spanMap := spanMaps[0] + spans := make([]int, len(opt.Header)) + for index := range spans { + v, ok := spanMap[index] + if !ok { + v = 1 + } + spans[index] = v + } + opt.Spans = spans + } + return TableOptionRender(opt) +} + +// TableOptionRender table render with option +func TableOptionRender(opt TableChartOption) (*Painter, error) { if opt.Type == "" { opt.Type = ChartOutputPNG } if opt.Width <= 0 { opt.Width = defaultChartWidth } - if opt.Height <= 0 { - opt.Height = defaultChartHeight - } if opt.Font == nil { opt.Font, _ = chart.GetDefaultFont() } p, err := NewPainter(PainterOptions{ - Type: opt.Type, - Width: opt.Width, - Height: opt.Height, + Type: opt.Type, + Width: opt.Width, + // 仅用于计算表格高度,因此随便设置即可 + Height: 100, Font: opt.Font, }) if err != nil { @@ -365,8 +384,6 @@ func TableRender(opt TableChartOption) (*Painter, error) { if err != nil { return nil, err } - fmt.Println(*info) - fmt.Println(info.Height) p, err = NewPainter(PainterOptions{ Type: opt.Type, @@ -377,31 +394,9 @@ func TableRender(opt TableChartOption) (*Painter, error) { if err != nil { return nil, err } - _, err = NewTableChart(p, opt).Render() + _, err = NewTableChart(p, opt).renderWithInfo(info) if err != nil { return nil, err } - - // opt := ChartOption{} - // for _, fn := range opts { - // fn(&opt) - // } - // opt.fillDefault() - // p, err := NewPainter(PainterOptions{ - // Type: opt.Type, - // Width: opt.Width, - // Height: opt.Height, - // Font: opt.font, - // }) - // if err != nil { - // return nil, err - // } - // _, err = NewTableChart(p, TableChartOption{ - // Header: header, - // Data: data, - // }).Render() - // if err != nil { - // return nil, err - // } return p, nil } diff --git a/examples/charts/main.go b/examples/charts/main.go index 0e1d48e..7b14919 100644 --- a/examples/charts/main.go +++ b/examples/charts/main.go @@ -92,6 +92,48 @@ func handler(w http.ResponseWriter, req *http.Request, chartOptions []charts.Cha bytesList = append(bytesList, buf) } + p, err := charts.TableOptionRender(charts.TableChartOption{ + Type: charts.ChartOutputSVG, + Header: []string{ + "Name", + "Age", + "Address", + "Tag", + "Action", + }, + Data: [][]string{ + { + "John Brown", + "32", + "New York No. 1 Lake Park", + "nice, developer", + "Send Mail", + }, + { + "Jim Green ", + "42", + "London No. 1 Lake Park", + "wow", + "Send Mail", + }, + { + "Joe Black ", + "32", + "Sidney No. 1 Lake Park", + "cool, teacher", + "Send Mail", + }, + }, + }) + if err != nil { + panic(err) + } + buf, err := p.Bytes() + if err != nil { + panic(err) + } + bytesList = append(bytesList, buf) + data := bytes.ReplaceAll([]byte(html), []byte("{{body}}"), bytes.Join(bytesList, []byte(""))) w.Header().Set("Content-Type", "text/html") w.Write(data) diff --git a/examples/table/main.go b/examples/table/main.go index 650566c..ee8147e 100644 --- a/examples/table/main.go +++ b/examples/table/main.go @@ -24,45 +24,49 @@ func writeFile(buf []byte) error { } func main() { - p, err := charts.TableRender(charts.TableChartOption{ - Header: []string{ - "Name", - "Age", - "Address", - "Tag", - "Action", + charts.SetDefaultWidth(810) + header := []string{ + "Name", + "Age", + "Address", + "Tag", + "Action", + } + data := [][]string{ + { + "John Brown", + "32", + "New York No. 1 Lake Park", + "nice, developer", + "Send Mail", }, - Spans: []int{ - 1, - 1, - 2, - 1, - 1, + { + "Jim Green ", + "42", + "London No. 1 Lake Park", + "wow", + "Send Mail", }, - Data: [][]string{ - { - "John Brown", - "32", - "New York No. 1 Lake Park", - "nice, developer", - "Send Mail", - }, - { - "Jim Green ", - "42", - "London No. 1 Lake Park", - "wow", - "Send Mail", - }, - { - "Joe Black ", - "32", - "Sidney No. 1 Lake Park", - "cool, teacher", - "Send Mail", - }, + { + "Joe Black ", + "32", + "Sidney No. 1 Lake Park", + "cool, teacher", + "Send Mail", }, - }, + } + spans := map[int]int{ + 0: 2, + 1: 1, + // 设置第三列的span + 2: 3, + 3: 2, + 4: 2, + } + p, err := charts.TableRender( + header, + data, + spans, ) if err != nil { panic(err) diff --git a/painter.go b/painter.go index c787315..06973b6 100644 --- a/painter.go +++ b/painter.go @@ -38,6 +38,8 @@ type Painter struct { parent *Painter style Style theme ColorPalette + // 类型 + outputType string } type PainterOptions struct { @@ -169,6 +171,8 @@ func NewPainter(opts PainterOptions, opt ...PainterOption) (*Painter, error) { Bottom: opts.Height, }, font: font, + // 类型 + outputType: opts.Type, } p.setOptions(opt...) if p.theme == nil { diff --git a/table.go b/table.go index 34dee67..ffa7013 100644 --- a/table.go +++ b/table.go @@ -35,6 +35,7 @@ type tableChart struct { opt *TableChartOption } +// NewTableChart returns a table chart render func NewTableChart(p *Painter, opt TableChartOption) *tableChart { if opt.Theme == nil { opt.Theme = defaultTheme @@ -50,8 +51,6 @@ type TableChartOption struct { Type string // The width of table Width int - // The height of table - Height int // The theme Theme ColorPalette // The padding of table cell @@ -64,7 +63,9 @@ type TableChartOption struct { Spans []int // The font size of table FontSize float64 - Font *truetype.Font + // The font family, which should be installed first + FontFamily string + Font *truetype.Font // The font color of table FontColor Color // The background color of header @@ -77,26 +78,101 @@ type TableChartOption struct { BackgroundColor Color } -var defaultTableHeaderColor = Color{ - R: 34, - G: 34, - B: 34, - A: 255, +type TableSetting struct { + // The color of header + HeaderColor Color + // The color of heder text + HeaderFontColor Color + // The color of table text + FontColor Color + // The color list of row + RowColors []Color + // The padding of cell + Padding Box } -var defaultTableRowColors = []Color{ - drawing.ColorWhite, - { - R: 242, - G: 242, - B: 242, + +var TableLightThemeSetting = TableSetting{ + HeaderColor: Color{ + R: 240, + G: 240, + B: 240, A: 255, }, + HeaderFontColor: Color{ + R: 98, + G: 105, + B: 118, + A: 255, + }, + FontColor: Color{ + R: 70, + G: 70, + B: 70, + A: 255, + }, + RowColors: []Color{ + drawing.ColorWhite, + { + R: 247, + G: 247, + B: 247, + A: 255, + }, + }, + Padding: Box{ + Left: 10, + Top: 10, + Right: 10, + Bottom: 10, + }, } -var defaultTablePadding = Box{ - Left: 10, - Top: 10, - Right: 10, - Bottom: 10, + +var TableDarkThemeSetting = TableSetting{ + HeaderColor: Color{ + R: 38, + G: 38, + B: 42, + A: 255, + }, + HeaderFontColor: Color{ + R: 216, + G: 217, + B: 218, + A: 255, + }, + FontColor: Color{ + R: 216, + G: 217, + B: 218, + A: 255, + }, + RowColors: []Color{ + { + R: 24, + G: 24, + B: 28, + A: 255, + }, + { + R: 38, + G: 38, + B: 42, + A: 255, + }, + }, + Padding: Box{ + Left: 10, + Top: 10, + Right: 10, + Bottom: 10, + }, +} + +var tableDefaultSetting = TableLightThemeSetting + +// SetDefaultTableSetting sets the default setting for table +func SetDefaultTableSetting(setting TableSetting) { + tableDefaultSetting = setting } type renderInfo struct { @@ -106,12 +182,12 @@ type renderInfo struct { RowHeights []int } -func (c *tableChart) render() (*renderInfo, error) { +func (t *tableChart) render() (*renderInfo, error) { info := renderInfo{ RowHeights: make([]int, 0), } - p := c.p - opt := c.opt + p := t.p + opt := t.opt if len(opt.Header) == 0 { return nil, errors.New("header can not be nil") } @@ -125,7 +201,7 @@ func (c *tableChart) render() (*renderInfo, error) { } fontColor := opt.FontColor if fontColor.IsZero() { - fontColor = theme.GetTextColor() + fontColor = tableDefaultSetting.FontColor } font := opt.Font if font == nil { @@ -133,14 +209,14 @@ func (c *tableChart) render() (*renderInfo, error) { } headerFontColor := opt.HeaderFontColor if opt.HeaderFontColor.IsZero() { - headerFontColor = drawing.ColorWhite + headerFontColor = tableDefaultSetting.HeaderFontColor } spans := opt.Spans - if len(spans) != 0 && len(spans) != len(opt.Header) { + if len(spans) != len(opt.Header) { newSpans := make([]int, len(opt.Header)) for index := range opt.Header { - if len(spans) < index { + if index >= len(spans) { newSpans[index] = 1 } else { newSpans[index] = spans[index] @@ -149,7 +225,8 @@ func (c *tableChart) render() (*renderInfo, error) { spans = newSpans } - values := autoDivideSpans(p.Width(), len(opt.Header)+1, spans) + sum := sumInt(spans) + values := autoDivideSpans(p.Width(), sum, spans) height := 0 textStyle := Style{ FontSize: fontSize, @@ -162,7 +239,7 @@ func (c *tableChart) render() (*renderInfo, error) { headerHeight := 0 padding := opt.Padding if padding.IsZero() { - padding = defaultTablePadding + padding = tableDefaultSetting.Padding } renderTableCells := func(textList []string, currentHeight int, cellPadding Box) int { @@ -201,34 +278,23 @@ func (c *tableChart) render() (*renderInfo, error) { return &info, nil } -func (c *tableChart) Render() (Box, error) { - p := c.p - opt := c.opt +func (t *tableChart) renderWithInfo(info *renderInfo) (Box, error) { + p := t.p + opt := t.opt if !opt.BackgroundColor.IsZero() { p.SetBackground(p.Width(), p.Height(), opt.BackgroundColor) } headerBGColor := opt.HeaderBackgroundColor if headerBGColor.IsZero() { - headerBGColor = defaultTableHeaderColor + headerBGColor = tableDefaultSetting.HeaderColor } - r := p.render - newRender, err := chart.SVG(p.Width(), 100) - if err != nil { - return BoxZero, err - } - p.render = newRender - info, err := c.render() - if err != nil { - return BoxZero, err - } - p.render = r // 如果设置表头背景色 p.SetBackground(info.Width, info.HeaderHeight, headerBGColor, true) currentHeight := info.HeaderHeight rowColors := opt.RowBackgroundColors if len(rowColors) == 0 { - rowColors = defaultTableRowColors + rowColors = tableDefaultSetting.RowColors } for index, h := range info.RowHeights { color := rowColors[index%len(rowColors)] @@ -238,7 +304,7 @@ func (c *tableChart) Render() (Box, error) { child.SetBackground(p.Width(), h, color, true) currentHeight += h } - _, err = c.render() + _, err := t.render() if err != nil { return BoxZero, err } @@ -248,3 +314,35 @@ func (c *tableChart) Render() (Box, error) { Bottom: info.Height, }, nil } + +func (t *tableChart) Render() (Box, error) { + p := t.p + opt := t.opt + if !opt.BackgroundColor.IsZero() { + p.SetBackground(p.Width(), p.Height(), opt.BackgroundColor) + } + headerBGColor := opt.HeaderBackgroundColor + if headerBGColor.IsZero() { + headerBGColor = tableDefaultSetting.HeaderColor + } + if opt.Font == nil && opt.FontFamily != "" { + opt.Font, _ = GetFont(opt.FontFamily) + } + + r := p.render + fn := chart.PNG + if p.outputType == ChartOutputSVG { + fn = chart.SVG + } + newRender, err := fn(p.Width(), 100) + if err != nil { + return BoxZero, err + } + p.render = newRender + info, err := t.render() + if err != nil { + return BoxZero, err + } + p.render = r + return t.renderWithInfo(info) +} diff --git a/table_test.go b/table_test.go index c54de25..41a857c 100644 --- a/table_test.go +++ b/table_test.go @@ -51,7 +51,8 @@ func TestTableChart(t *testing.T) { 1, 2, 1, - 1, + // span和header不匹配,最后自动设置为1 + // 1, }, Data: [][]string{ { @@ -82,6 +83,48 @@ func TestTableChart(t *testing.T) { } return p.Bytes() }, + result: "\\nNameAgeAddressTagActionJohnBrown32New York No. 1 Lake Parknice,developerSend MailJim Green42London No. 1 Lake ParkwowSend MailJoe Black32Sidney No. 1 Lake Parkcool,teacherSend Mail", + }, + { + render: func(p *Painter) ([]byte, error) { + _, err := NewTableChart(p, TableChartOption{ + Header: []string{ + "Name", + "Age", + "Address", + "Tag", + "Action", + }, + Data: [][]string{ + { + "John Brown", + "32", + "New York No. 1 Lake Park", + "nice, developer", + "Send Mail", + }, + { + "Jim Green ", + "42", + "London No. 1 Lake Park", + "wow", + "Send Mail", + }, + { + "Joe Black ", + "32", + "Sidney No. 1 Lake Park", + "cool, teacher", + "Send Mail", + }, + }, + }).Render() + if err != nil { + return nil, err + } + return p.Bytes() + }, + result: "\\nNameAgeAddressTagActionJohn Brown32New York No.1 Lake Parknice,developerSend MailJim Green42London No. 1Lake ParkwowSend MailJoe Black32Sidney No. 1Lake Parkcool, teacherSend Mail", }, } for _, tt := range tests { @@ -96,5 +139,4 @@ func TestTableChart(t *testing.T) { fmt.Println(string(data)) assert.Equal(tt.result, string(data)) } - } From da3ad16c23abb669361295e92a38680409dbe01e Mon Sep 17 00:00:00 2001 From: vicanso Date: Sat, 25 Jun 2022 08:23:50 +0800 Subject: [PATCH 035/116] chore: upload table preview --- assets/go-table.png | Bin 0 -> 17387 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 assets/go-table.png diff --git a/assets/go-table.png b/assets/go-table.png new file mode 100644 index 0000000000000000000000000000000000000000..5848ad88b41c8266171e4211002499fea1244304 GIT binary patch literal 17387 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 literal 0 HcmV?d00001 From f1276067d763e612b927c839c7e29b5af9ff7a23 Mon Sep 17 00:00:00 2001 From: vicanso Date: Sat, 25 Jun 2022 08:33:05 +0800 Subject: [PATCH 036/116] fix: fix lint --- README.md | 3 ++- README_zh.md | 6 +++++- table.go | 4 ---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index d0549f0..3e713a6 100644 --- a/README.md +++ b/README.md @@ -18,9 +18,10 @@ Screenshot of common charts, the left part is light theme, the right part is gra

go-table

+ ## Chart Type -These chart types are supported: `line`, `bar`, `pie`, `radar` or `funnel`. +These chart types are supported: `line`, `bar`, `pie`, `radar` or `funnel` and `table`. ## Example diff --git a/README_zh.md b/README_zh.md index 487a365..94a9e14 100644 --- a/README_zh.md +++ b/README_zh.md @@ -12,9 +12,13 @@ go-charts

+

+ go-table +

Date: Sat, 25 Jun 2022 08:49:00 +0800 Subject: [PATCH 037/116] test: add bench mark --- charts_test.go | 255 +++++++++++++++++++++++++++++++++++++++++++++++++ table_test.go | 2 - 2 files changed, 255 insertions(+), 2 deletions(-) create mode 100644 charts_test.go diff --git a/charts_test.go b/charts_test.go new file mode 100644 index 0000000..da75ee5 --- /dev/null +++ b/charts_test.go @@ -0,0 +1,255 @@ +// 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 ( + "errors" + "testing" + + "github.com/wcharczuk/go-chart/v2" +) + +func BenchmarkMultiChartPNGRender(b *testing.B) { + for i := 0; i < b.N; i++ { + opt := ChartOption{ + Type: ChartOutputPNG, + Legend: LegendOption{ + Top: "-90", + Data: []string{ + "Milk Tea", + "Matcha Latte", + "Cheese Cocoa", + "Walnut Brownie", + }, + }, + Padding: chart.Box{ + Top: 100, + Right: 10, + Bottom: 10, + Left: 10, + }, + XAxis: NewXAxisOption([]string{ + "2012", + "2013", + "2014", + "2015", + "2016", + "2017", + }), + YAxisOptions: []YAxisOption{ + { + + Min: NewFloatPoint(0), + Max: NewFloatPoint(90), + }, + }, + SeriesList: []Series{ + NewSeriesFromValues([]float64{ + 56.5, + 82.1, + 88.7, + 70.1, + 53.4, + 85.1, + }), + NewSeriesFromValues([]float64{ + 51.1, + 51.4, + 55.1, + 53.3, + 73.8, + 68.7, + }), + NewSeriesFromValues([]float64{ + 40.1, + 62.2, + 69.5, + 36.4, + 45.2, + 32.5, + }, ChartTypeBar), + NewSeriesFromValues([]float64{ + 25.2, + 37.1, + 41.2, + 18, + 33.9, + 49.1, + }, ChartTypeBar), + }, + Children: []ChartOption{ + { + Legend: LegendOption{ + Show: FalseFlag(), + Data: []string{ + "Milk Tea", + "Matcha Latte", + "Cheese Cocoa", + "Walnut Brownie", + }, + }, + Box: chart.Box{ + Top: 20, + Left: 400, + Right: 500, + Bottom: 120, + }, + SeriesList: NewPieSeriesList([]float64{ + 435.9, + 354.3, + 285.9, + 204.5, + }, PieSeriesOption{ + Label: SeriesLabel{ + Show: true, + }, + Radius: "35%", + }), + }, + }, + } + d, err := Render(opt) + if err != nil { + panic(err) + } + buf, err := d.Bytes() + if err != nil { + panic(err) + } + if len(buf) == 0 { + panic(errors.New("data is nil")) + } + } +} + +func BenchmarkMultiChartSVGRender(b *testing.B) { + for i := 0; i < b.N; i++ { + opt := ChartOption{ + Legend: LegendOption{ + Top: "-90", + Data: []string{ + "Milk Tea", + "Matcha Latte", + "Cheese Cocoa", + "Walnut Brownie", + }, + }, + Padding: chart.Box{ + Top: 100, + Right: 10, + Bottom: 10, + Left: 10, + }, + XAxis: NewXAxisOption([]string{ + "2012", + "2013", + "2014", + "2015", + "2016", + "2017", + }), + YAxisOptions: []YAxisOption{ + { + + Min: NewFloatPoint(0), + Max: NewFloatPoint(90), + }, + }, + SeriesList: []Series{ + NewSeriesFromValues([]float64{ + 56.5, + 82.1, + 88.7, + 70.1, + 53.4, + 85.1, + }), + NewSeriesFromValues([]float64{ + 51.1, + 51.4, + 55.1, + 53.3, + 73.8, + 68.7, + }), + NewSeriesFromValues([]float64{ + 40.1, + 62.2, + 69.5, + 36.4, + 45.2, + 32.5, + }, ChartTypeBar), + NewSeriesFromValues([]float64{ + 25.2, + 37.1, + 41.2, + 18, + 33.9, + 49.1, + }, ChartTypeBar), + }, + Children: []ChartOption{ + { + Legend: LegendOption{ + Show: FalseFlag(), + Data: []string{ + "Milk Tea", + "Matcha Latte", + "Cheese Cocoa", + "Walnut Brownie", + }, + }, + Box: chart.Box{ + Top: 20, + Left: 400, + Right: 500, + Bottom: 120, + }, + SeriesList: NewPieSeriesList([]float64{ + 435.9, + 354.3, + 285.9, + 204.5, + }, PieSeriesOption{ + Label: SeriesLabel{ + Show: true, + }, + Radius: "35%", + }), + }, + }, + } + d, err := Render(opt) + if err != nil { + panic(err) + } + buf, err := d.Bytes() + if err != nil { + panic(err) + } + if len(buf) == 0 { + panic(errors.New("data is nil")) + } + } +} diff --git a/table_test.go b/table_test.go index 41a857c..a958c95 100644 --- a/table_test.go +++ b/table_test.go @@ -23,7 +23,6 @@ package charts import ( - "fmt" "testing" "github.com/stretchr/testify/assert" @@ -136,7 +135,6 @@ func TestTableChart(t *testing.T) { assert.Nil(err) data, err := tt.render(p) assert.Nil(err) - fmt.Println(string(data)) assert.Equal(tt.result, string(data)) } } From aed2250cb847817cf46508c4798cc17816f53ef5 Mon Sep 17 00:00:00 2001 From: vicanso Date: Sat, 25 Jun 2022 08:51:49 +0800 Subject: [PATCH 038/116] docs: update documents --- README.md | 2 +- README_zh.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3e713a6..1e4ea8b 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Screenshot of common charts, the left part is light theme, the right part is gra ## Chart Type -These chart types are supported: `line`, `bar`, `pie`, `radar` or `funnel` and `table`. +These chart types are supported: `line`, `bar`, `horizontal bar`, `pie`, `radar` or `funnel` and `table`. ## Example diff --git a/README_zh.md b/README_zh.md index 94a9e14..87c42fa 100644 --- a/README_zh.md +++ b/README_zh.md @@ -18,7 +18,7 @@ ## 支持图表类型 -支持以下的图表类型:`line`, `bar`, `pie`, `radar`, `funnel` 以及 `table` +支持以下的图表类型:`line`, `bar`, `horizontal bar`, `pie`, `radar`, `funnel` 以及 `table` ## 示例 From 0eecb6c5b7002dba33c8b2d7a9c06341f8395d25 Mon Sep 17 00:00:00 2001 From: vicanso Date: Sat, 25 Jun 2022 09:06:50 +0800 Subject: [PATCH 039/116] docs: update document --- assets/go-table.png | Bin 17387 -> 37028 bytes examples/table/main.go | 1 + 2 files changed, 1 insertion(+) diff --git a/assets/go-table.png b/assets/go-table.png index 5848ad88b41c8266171e4211002499fea1244304..b05a3d02eac00825c3125810803aab41e4d70a4e 100644 GIT binary patch literal 37028 zcmaI7bx<5Z*YJzG6WoF)xVr@nZoy@7ch@BlT!Onh!QEkTcY?cH2oQW>;oIlA@B7uQ z`^W8?+M4d|nQA$G`kY^%iBeOM!$c!RgMop;l>aKN0Rsci1Oo%hf&vFEDR!JIfq_Y| zmzVzX%?I|+GAAqy3Xon008r!+yAEo z$7@Eit)m!qa78~no@8JOnw~QTp}#)p@0zOb^NG2#HCp_xJayXh1M>U#e`b1xU9hm@ z&$EoxXJ%(@{fas+DT+6QyWV)&fApIO2nn^a@A=Kw{W~v97)!|+P-uG#Ng@S{2VRyr zwR7t3tV&m>OxHMOH9&d(H4#qZ!#bZB&T8V zZ9lf7h@Jcf(&t~Isl3zwGWvPEe3aHF=R!-5v~01k-oJqGG;V7T=;DA|{-muzE3cVmHn@+sy8_vb?+ffdsAhv>#8#= z0d)1esKTzYzgQO+Zqr?yTb|#VO4Qr)BO1 zM0_6zv3(r`t z6_OIkhS;N`_WkNmGb2PuK)|Bqgi!1L4igvW__s|UJZ*sBD`aa+v^P1i;j>UbbVvaz zUO+)7NdF$^*|%$0CcCUEI|GLDlg;quW&S4sVkz%y-N6BVUAY4X*wfE%M4RO&-`LPDC)@J&|M}gKIoWH6V0k`ZTyroA>*F42Ys`OqUuWb1r)FurPUfbsHZU?+ zi=3sC^MN)4%WVr|Ui{AO63%@nARuseqgoV*jlndmx zWM@On1Q~b{H1pn{XV8I-D`nQlJsEZVrz-;s6EzY2Dn`?6bLw{-%xYl0h{E|_-vqWG zPT;bJ4z_*c5`f~j0EQBU*Ho-w0{Su>Q#U)*7?m+M;jnV+6H6XuP!8pCxDC_iSG|0t zg;Syx*ASs!2%+>YD(&;^bl?F`tU&OBr<>clxB$(<{1?hi?)( zVN5u~`{^uXXP!2~+uB?F#9P}~Q2D7vesaY&aPAkCv*^b5w&hM^9S}ry*xXo;gfo=< zotTfw!Jq@>PO6{{9j93yb7(M(ji|MS4yQgn;|f z`4JwH0?ZsC%nFlm6^c`x36(CLE14mQOZxJchZAt)y?K^?eoK^iO7FmS&9@||0D6j- zvGioFy3if4K?-AhwyTr{?P_e^FI69v03ylHgr?)a;Eu<7Jz`aU6jbrx`N)!(7k<2A zr=5r{8YCA)BvcoJad}PmalPb76D2cJdBRJdf6d=XLu$@XR0CXEoTc9qNgRpImZpEr&InAmkcb7t&p1hT{x(BBnuLN`-Se4l!rV^d$@8>(i+uh@aOfP209-i z8t&28+$+OsdpS9|eKix5tc&3kOj(ewO3LD*rmfX++Kq0qVpW~9LkHUkHQ0_oJ1c*I zeR1`g+ONGyKAD+j0vYpwi6=dnmh8-_=Bd(P&IuP%h@};cHB_3-GWlcyiE>Ojrxro1;5TK2cQ7vXW`-o>O?HE7|n%nrsI zOZ&-z68M+Doww^>-wU9Z|3saV!-1!x|3njIq+CdIY)RAWYvK@~FEa33^o%HtBd=S{ zu0##mGy3Y(@5AWaRH)LaLd*}&3+yzxXV3Re-#2tpa*PX>Th2SRG3%uW%uW}G-=V4x zsS9JUah_#)X_o>raG@+-jSdd0wOYH7q`&+jbVl)}0#Y)Mk5yI>Dj~Wu3>HJt0B5}RO$a=#8HNY9bVb#&f;p8c3thmOui zM%i~tzszEBC=dEABBimdoYhEdrnjAUr|9^S5&F∓=KP*rXQ_7C$MY4kkTJ>x@hK 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%0A4C`wRf5K}TaGSwU<#E097z}`w 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 diff --git a/examples/table/main.go b/examples/table/main.go index ee8147e..4207b01 100644 --- a/examples/table/main.go +++ b/examples/table/main.go @@ -24,6 +24,7 @@ func writeFile(buf []byte) error { } func main() { + // charts.SetDefaultTableSetting(charts.TableDarkThemeSetting) charts.SetDefaultWidth(810) header := []string{ "Name", From d53fa1a329bed63efb02afdcfbb4cc338f233990 Mon Sep 17 00:00:00 2001 From: vicanso Date: Tue, 28 Jun 2022 20:21:06 +0800 Subject: [PATCH 040/116] feat: support customize table cell style --- examples/table/main.go | 55 ++++++++++++++++++++++-- table.go | 96 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 142 insertions(+), 9 deletions(-) diff --git a/examples/table/main.go b/examples/table/main.go index 4207b01..f332851 100644 --- a/examples/table/main.go +++ b/examples/table/main.go @@ -4,18 +4,20 @@ import ( "io/ioutil" "os" "path/filepath" + "strconv" "github.com/vicanso/go-charts/v2" + "github.com/wcharczuk/go-chart/v2/drawing" ) -func writeFile(buf []byte) error { +func writeFile(buf []byte, filename string) error { tmpPath := "./tmp" err := os.MkdirAll(tmpPath, 0700) if err != nil { return err } - file := filepath.Join(tmpPath, "table.png") + file := filepath.Join(tmpPath, filename) err = ioutil.WriteFile(file, buf, 0600) if err != nil { return err @@ -77,7 +79,54 @@ func main() { if err != nil { panic(err) } - err = writeFile(buf) + err = writeFile(buf, "table.png") + if err != nil { + panic(err) + } + + p, err = charts.TableOptionRender(charts.TableChartOption{ + Header: header, + Data: data, + CellTextStyle: func(tc charts.TableCell) *charts.Style { + row := tc.Row + column := tc.Column + style := tc.Style + if column == 1 && row != 0 { + age, _ := strconv.Atoi(tc.Text) + if age < 40 { + style.FontColor = drawing.ColorGreen + } else { + style.FontColor = drawing.ColorRed + } + return &style + } + return nil + }, + CellStyle: func(tc charts.TableCell) *charts.Style { + row := tc.Row + column := tc.Column + if row == 2 && column == 1 { + return &charts.Style{ + FillColor: drawing.ColorBlue, + } + } + if row == 3 && column == 4 { + return &charts.Style{ + FillColor: drawing.ColorRed.WithAlpha(100), + } + } + return nil + }, + }) + if err != nil { + panic(err) + } + + buf, err = p.Bytes() + if err != nil { + panic(err) + } + err = writeFile(buf, "table-color.png") if err != nil { panic(err) } diff --git a/table.go b/table.go index 0e72be5..d1af5f9 100644 --- a/table.go +++ b/table.go @@ -46,6 +46,17 @@ func NewTableChart(p *Painter, opt TableChartOption) *tableChart { } } +type TableCell struct { + // Text the text of table cell + Text string + // Style the current style of table cell + Style Style + // Row the row index of table cell + Row int + // Column the column index of table cell + Column int +} + type TableChartOption struct { // The output type Type string @@ -76,6 +87,10 @@ type TableChartOption struct { RowBackgroundColors []Color // The background color BackgroundColor Color + // CellTextStyle customize text style of table cell + CellTextStyle func(TableCell) *Style + // CellStyle customize drawing style of table cell + CellStyle func(TableCell) *Style } type TableSetting struct { @@ -180,6 +195,7 @@ type renderInfo struct { Height int HeaderHeight int RowHeights []int + ColumnWidths []int } func (t *tableChart) render() (*renderInfo, error) { @@ -227,6 +243,15 @@ func (t *tableChart) render() (*renderInfo, error) { sum := sumInt(spans) values := autoDivideSpans(p.Width(), sum, spans) + columnWidths := make([]int, 0) + for index, v := range values { + if index == len(values)-1 { + break + } + columnWidths = append(columnWidths, values[index+1]-v) + } + info.ColumnWidths = columnWidths + height := 0 textStyle := Style{ FontSize: fontSize, @@ -234,25 +259,48 @@ func (t *tableChart) render() (*renderInfo, error) { FillColor: headerFontColor, Font: font, } - p.SetStyle(textStyle) headerHeight := 0 padding := opt.Padding if padding.IsZero() { padding = tableDefaultSetting.Padding } + getCellTextStyle := opt.CellTextStyle + if getCellTextStyle == nil { + getCellTextStyle = func(_ TableCell) *Style { + return nil + } + } - renderTableCells := func(textList []string, currentHeight int, cellPadding Box) int { + // 表格单元的处理 + renderTableCells := func( + currentStyle Style, + rowIndex int, + textList []string, + currentHeight int, + cellPadding Box, + ) int { cellMaxHeight := 0 paddingHeight := cellPadding.Top + cellPadding.Bottom paddingWidth := cellPadding.Left + cellPadding.Right for index, text := range textList { + cellStyle := getCellTextStyle(TableCell{ + Text: text, + Row: rowIndex, + Column: index, + Style: currentStyle, + }) + if cellStyle == nil { + cellStyle = ¤tStyle + } + p.SetStyle(*cellStyle) x := values[index] y := currentHeight + cellPadding.Top width := values[index+1] - x x += cellPadding.Left width -= paddingWidth box := p.TextFit(text, x, y+int(fontSize), width) + // 计算最高的高度 if box.Height()+paddingHeight > cellMaxHeight { cellMaxHeight = box.Height() + paddingHeight } @@ -260,15 +308,16 @@ func (t *tableChart) render() (*renderInfo, error) { return cellMaxHeight } - headerHeight = renderTableCells(opt.Header, height, padding) + // 表头的处理 + headerHeight = renderTableCells(textStyle, 0, opt.Header, height, padding) height += headerHeight info.HeaderHeight = headerHeight + // 表格内容的处理 textStyle.FontColor = fontColor textStyle.FillColor = fontColor - p.SetStyle(textStyle) - for _, textList := range opt.Data { - cellHeight := renderTableCells(textList, height, padding) + for index, textList := range opt.Data { + cellHeight := renderTableCells(textStyle, index+1, textList, height, padding) info.RowHeights = append(info.RowHeights, cellHeight) height += cellHeight } @@ -304,6 +353,41 @@ func (t *tableChart) renderWithInfo(info *renderInfo) (Box, error) { child.SetBackground(p.Width(), h, color, true) currentHeight += h } + // 根据是否有设置表格样式调整背景色 + getCellStyle := opt.CellStyle + if getCellStyle != nil { + arr := [][]string{ + opt.Header, + } + arr = append(arr, opt.Data...) + top := 0 + heights := []int{ + info.HeaderHeight, + } + heights = append(heights, info.RowHeights...) + // 循环所有表格单元,生成背景色 + for i, textList := range arr { + left := 0 + for j, v := range textList { + style := getCellStyle(TableCell{ + Text: v, + Row: i, + Column: j, + }) + if style != nil && !style.FillColor.IsZero() { + child := p.Child(PainterPaddingOption(Box{ + Top: top, + Left: left, + })) + w := info.ColumnWidths[j] + h := heights[i] + child.SetBackground(w, h, style.FillColor, true) + } + left += info.ColumnWidths[j] + } + top += heights[i] + } + } _, err := t.render() if err != nil { return BoxZero, err From f483e2a850e363b848fd09d4521266fdc608e41a Mon Sep 17 00:00:00 2001 From: vicanso Date: Wed, 29 Jun 2022 20:15:58 +0800 Subject: [PATCH 041/116] feat: support text align for table cell --- assets/go-table.png | Bin 37028 -> 30679 bytes examples/table/main.go | 98 ++++++++++++++++++++++++++++++----------- painter.go | 14 +++++- table.go | 24 +++++++--- 4 files changed, 101 insertions(+), 35 deletions(-) diff --git a/assets/go-table.png b/assets/go-table.png index b05a3d02eac00825c3125810803aab41e4d70a4e..88ec49c43ae25b60269ad5800bc06154d491c194 100644 GIT binary patch literal 30679 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 RhT9cA%JLd=r84Fp{|_XcF>?R_ literal 37028 zcmaI7bx<5Z*YJzG6WoF)xVr@nZoy@7ch@BlT!Onh!QEkTcY?cH2oQW>;oIlA@B7uQ z`^W8?+M4d|nQA$G`kY^%iBeOM!$c!RgMop;l>aKN0Rsci1Oo%hf&vFEDR!JIfq_Y| zmzVzX%?I|+GAAqy3Xon008r!+yAEo z$7@Eit)m!qa78~no@8JOnw~QTp}#)p@0zOb^NG2#HCp_xJayXh1M>U#e`b1xU9hm@ z&$EoxXJ%(@{fas+DT+6QyWV)&fApIO2nn^a@A=Kw{W~v97)!|+P-uG#Ng@S{2VRyr zwR7t3tV&m>OxHMOH9&d(H4#qZ!#bZB&T8V zZ9lf7h@Jcf(&t~Isl3zwGWvPEe3aHF=R!-5v~01k-oJqGG;V7T=;DA|{-muzE3cVmHn@+sy8_vb?+ffdsAhv>#8#= z0d)1esKTzYzgQO+Zqr?yTb|#VO4Qr)BO1 zM0_6zv3(r`t z6_OIkhS;N`_WkNmGb2PuK)|Bqgi!1L4igvW__s|UJZ*sBD`aa+v^P1i;j>UbbVvaz zUO+)7NdF$^*|%$0CcCUEI|GLDlg;quW&S4sVkz%y-N6BVUAY4X*wfE%M4RO&-`LPDC)@J&|M}gKIoWH6V0k`ZTyroA>*F42Ys`OqUuWb1r)FurPUfbsHZU?+ zi=3sC^MN)4%WVr|Ui{AO63%@nARuseqgoV*jlndmx zWM@On1Q~b{H1pn{XV8I-D`nQlJsEZVrz-;s6EzY2Dn`?6bLw{-%xYl0h{E|_-vqWG zPT;bJ4z_*c5`f~j0EQBU*Ho-w0{Su>Q#U)*7?m+M;jnV+6H6XuP!8pCxDC_iSG|0t zg;Syx*ASs!2%+>YD(&;^bl?F`tU&OBr<>clxB$(<{1?hi?)( zVN5u~`{^uXXP!2~+uB?F#9P}~Q2D7vesaY&aPAkCv*^b5w&hM^9S}ry*xXo;gfo=< zotTfw!Jq@>PO6{{9j93yb7(M(ji|MS4yQgn;|f z`4JwH0?ZsC%nFlm6^c`x36(CLE14mQOZxJchZAt)y?K^?eoK^iO7FmS&9@||0D6j- zvGioFy3if4K?-AhwyTr{?P_e^FI69v03ylHgr?)a;Eu<7Jz`aU6jbrx`N)!(7k<2A zr=5r{8YCA)BvcoJad}PmalPb76D2cJdBRJdf6d=XLu$@XR0CXEoTc9qNgRpImZpEr&InAmkcb7t&p1hT{x(BBnuLN`-Se4l!rV^d$@8>(i+uh@aOfP209-i z8t&28+$+OsdpS9|eKix5tc&3kOj(ewO3LD*rmfX++Kq0qVpW~9LkHUkHQ0_oJ1c*I zeR1`g+ONGyKAD+j0vYpwi6=dnmh8-_=Bd(P&IuP%h@};cHB_3-GWlcyiE>Ojrxro1;5TK2cQ7vXW`-o>O?HE7|n%nrsI zOZ&-z68M+Doww^>-wU9Z|3saV!-1!x|3njIq+CdIY)RAWYvK@~FEa33^o%HtBd=S{ zu0##mGy3Y(@5AWaRH)LaLd*}&3+yzxXV3Re-#2tpa*PX>Th2SRG3%uW%uW}G-=V4x zsS9JUah_#)X_o>raG@+-jSdd0wOYH7q`&+jbVl)}0#Y)Mk5yI>Dj~Wu3>HJt0B5}RO$a=#8HNY9bVb#&f;p8c3thmOui zM%i~tzszEBC=dEABBimdoYhEdrnjAUr|9^S5&F∓=KP*rXQ_7C$MY4kkTJ>x@hK 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%0A4C`wRf5K}TaGSwU<#E097z}`w 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 0 { + style.FillColor = charts.Color{ + R: 179, + G: 53, + B: 20, + A: 255, + } + } else if value < 0 { + style.FillColor = charts.Color{ + R: 33, + G: 124, + B: 50, + A: 255, } } - if row == 3 && column == 4 { - return &charts.Style{ - FillColor: drawing.ColorRed.WithAlpha(100), - } - } - return nil + return &style }, }) if err != nil { diff --git a/painter.go b/painter.go index 06973b6..62a4378 100644 --- a/painter.go +++ b/painter.go @@ -545,7 +545,7 @@ func (p *Painter) Text(body string, x, y int) *Painter { return p } -func (p *Painter) TextFit(body string, x, y, width int) chart.Box { +func (p *Painter) TextFit(body string, x, y, width int, textAligns ...string) chart.Box { style := p.style textWarp := style.TextWrap style.TextWrap = chart.TextWrapWord @@ -554,14 +554,24 @@ func (p *Painter) TextFit(body string, x, y, width int) chart.Box { p.SetTextStyle(style) var output chart.Box + textAlign := "" + if len(textAligns) != 0 { + textAlign = textAligns[0] + } for index, line := range lines { if line == "" { continue } x0 := x y0 := y + output.Height() - p.Text(line, x0, y0) lineBox := r.MeasureText(line) + switch textAlign { + case AlignRight: + x0 += width - lineBox.Width() + case AlignCenter: + x0 += (width - lineBox.Width()) >> 1 + } + p.Text(line, x0, y0) output.Right = chart.MaxInt(lineBox.Right, output.Right) output.Bottom += lineBox.Height() if index < len(lines)-1 { diff --git a/table.go b/table.go index d1af5f9..86ef569 100644 --- a/table.go +++ b/table.go @@ -70,8 +70,10 @@ type TableChartOption struct { Header []string // The data of table Data [][]string - // The span of table column + // The span list of table column Spans []int + // The text align list of table cell + TextAligns []string // The font size of table FontSize float64 // The font family, which should be installed first @@ -271,6 +273,13 @@ func (t *tableChart) render() (*renderInfo, error) { return nil } } + // textAligns := opt.TextAligns + getTextAlign := func(index int) string { + if len(opt.TextAligns) <= index { + return "" + } + return opt.TextAligns[index] + } // 表格单元的处理 renderTableCells := func( @@ -299,7 +308,7 @@ func (t *tableChart) render() (*renderInfo, error) { width := values[index+1] - x x += cellPadding.Left width -= paddingWidth - box := p.TextFit(text, x, y+int(fontSize), width) + box := p.TextFit(text, x, y+int(fontSize), width, getTextAlign(index)) // 计算最高的高度 if box.Height()+paddingHeight > cellMaxHeight { cellMaxHeight = box.Height() + paddingHeight @@ -342,7 +351,7 @@ func (t *tableChart) renderWithInfo(info *renderInfo) (Box, error) { p.SetBackground(info.Width, info.HeaderHeight, headerBGColor, true) currentHeight := info.HeaderHeight rowColors := opt.RowBackgroundColors - if len(rowColors) == 0 { + if rowColors == nil { rowColors = tableDefaultSetting.RowColors } for index, h := range info.RowHeights { @@ -375,12 +384,13 @@ func (t *tableChart) renderWithInfo(info *renderInfo) (Box, error) { Column: j, }) if style != nil && !style.FillColor.IsZero() { + padding := style.Padding child := p.Child(PainterPaddingOption(Box{ - Top: top, - Left: left, + Top: top + padding.Top, + Left: left + padding.Left, })) - w := info.ColumnWidths[j] - h := heights[i] + w := info.ColumnWidths[j] - padding.Left - padding.Top + h := heights[i] - padding.Top - padding.Bottom child.SetBackground(w, h, style.FillColor, true) } left += info.ColumnWidths[j] From c862467a5bc1a07f21c3d022c8f2368655dbb650 Mon Sep 17 00:00:00 2001 From: vicanso Date: Fri, 1 Jul 2022 20:41:55 +0800 Subject: [PATCH 042/116] fix: fix only one data of pie chart, #12 --- pie_chart.go | 145 ++++++++++++++++++++++++--------------------------- 1 file changed, 68 insertions(+), 77 deletions(-) diff --git a/pie_chart.go b/pie_chart.go index 6382140..0075ffc 100644 --- a/pie_chart.go +++ b/pie_chart.go @@ -99,88 +99,79 @@ func (p *pieChart) render(result *defaultRenderResult, seriesList SeriesList) (B seriesNames = seriesList.Names() } theme := opt.Theme - if len(values) == 1 { + + currentValue := float64(0) + prevEndX := 0 + prevEndY := 0 + for index, v := range values { seriesPainter.OverrideDrawingStyle(Style{ StrokeWidth: 1, - StrokeColor: theme.GetSeriesColor(0), - FillColor: theme.GetSeriesColor(0), + StrokeColor: theme.GetSeriesColor(index), + FillColor: theme.GetSeriesColor(index), }) - seriesPainter.MoveTo(cx, cy). - Circle(radius, cx, cy) - } else { - currentValue := float64(0) - prevEndX := 0 - prevEndY := 0 - for index, v := range values { - seriesPainter.OverrideDrawingStyle(Style{ - StrokeWidth: 1, - StrokeColor: theme.GetSeriesColor(index), - FillColor: theme.GetSeriesColor(index), - }) - seriesPainter.MoveTo(cx, cy) - start := chart.PercentToRadians(currentValue/total) - math.Pi/2 - currentValue += v - percent := (v / total) - delta := chart.PercentToRadians(percent) - seriesPainter.ArcTo(cx, cy, radius, radius, start, delta). - LineTo(cx, cy). - Close(). - FillStroke() + seriesPainter.MoveTo(cx, cy) + start := chart.PercentToRadians(currentValue/total) - math.Pi/2 + currentValue += v + percent := (v / total) + delta := chart.PercentToRadians(percent) + seriesPainter.ArcTo(cx, cy, radius, radius, start, delta). + LineTo(cx, cy). + Close(). + FillStroke() - series := seriesList[index] - // 是否显示label - showLabel := series.Label.Show - if !showLabel { - continue - } - - // label的角度为饼块中间 - angle := start + delta/2 - startx := cx + int(radius*math.Cos(angle)) - starty := cy + int(radius*math.Sin(angle)) - - endx := cx + int(labelRadius*math.Cos(angle)) - endy := cy + int(labelRadius*math.Sin(angle)) - // 计算是否有重叠,如果有则调整y坐标位置 - if index != 0 && - math.Abs(float64(endx-prevEndX)) < labelFontSize && - math.Abs(float64(endy-prevEndY)) < labelFontSize { - endy -= (labelFontSize << 1) - } - prevEndX = endx - prevEndY = endy - - seriesPainter.MoveTo(startx, starty) - seriesPainter.LineTo(endx, endy) - offset := labelLineWidth - if endx < cx { - offset *= -1 - } - seriesPainter.MoveTo(endx, endy) - endx += offset - seriesPainter.LineTo(endx, endy) - seriesPainter.Stroke() - - textStyle := Style{ - FontColor: theme.GetTextColor(), - FontSize: labelFontSize, - Font: opt.Font, - } - if !series.Label.Color.IsZero() { - textStyle.FontColor = series.Label.Color - } - seriesPainter.OverrideTextStyle(textStyle) - text := NewPieLabelFormatter(seriesNames, series.Label.Formatter)(index, v, percent) - textBox := seriesPainter.MeasureText(text) - textMargin := 3 - x := endx + textMargin - y := endy + textBox.Height()>>1 - 1 - if offset < 0 { - textWidth := textBox.Width() - x = endx - textWidth - textMargin - } - seriesPainter.Text(text, x, y) + series := seriesList[index] + // 是否显示label + showLabel := series.Label.Show + if !showLabel { + continue } + + // label的角度为饼块中间 + angle := start + delta/2 + startx := cx + int(radius*math.Cos(angle)) + starty := cy + int(radius*math.Sin(angle)) + + endx := cx + int(labelRadius*math.Cos(angle)) + endy := cy + int(labelRadius*math.Sin(angle)) + // 计算是否有重叠,如果有则调整y坐标位置 + if index != 0 && + math.Abs(float64(endx-prevEndX)) < labelFontSize && + math.Abs(float64(endy-prevEndY)) < labelFontSize { + endy -= (labelFontSize << 1) + } + prevEndX = endx + prevEndY = endy + + seriesPainter.MoveTo(startx, starty) + seriesPainter.LineTo(endx, endy) + offset := labelLineWidth + if endx < cx { + offset *= -1 + } + seriesPainter.MoveTo(endx, endy) + endx += offset + seriesPainter.LineTo(endx, endy) + seriesPainter.Stroke() + + textStyle := Style{ + FontColor: theme.GetTextColor(), + FontSize: labelFontSize, + Font: opt.Font, + } + if !series.Label.Color.IsZero() { + textStyle.FontColor = series.Label.Color + } + seriesPainter.OverrideTextStyle(textStyle) + text := NewPieLabelFormatter(seriesNames, series.Label.Formatter)(index, v, percent) + textBox := seriesPainter.MeasureText(text) + textMargin := 3 + x := endx + textMargin + y := endy + textBox.Height()>>1 - 1 + if offset < 0 { + textWidth := textBox.Width() + x = endx - textWidth - textMargin + } + seriesPainter.Text(text, x, y) } return p.p.box, nil From b56d0c546028facd0790ce334336129c88cba0e8 Mon Sep 17 00:00:00 2001 From: vicanso Date: Mon, 4 Jul 2022 20:39:10 +0800 Subject: [PATCH 043/116] fix: fix init fail for empty series list --- series.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/series.go b/series.go index 87a719f..ea71869 100644 --- a/series.go +++ b/series.go @@ -132,6 +132,9 @@ type Series struct { type SeriesList []Series func (sl SeriesList) init() { + if len(sl) == 0 { + return + } if sl[len(sl)-1].index != 0 { return } From eef3a2f97b1ca9d1e337a025a493ee24c8832af4 Mon Sep 17 00:00:00 2001 From: vicanso Date: Wed, 6 Jul 2022 20:28:46 +0800 Subject: [PATCH 044/116] fix: fix label overflow, #13 --- legend.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/legend.go b/legend.go index 2acd35b..d3b135e 100644 --- a/legend.go +++ b/legend.go @@ -135,7 +135,11 @@ func (l *legendPainter) Render() (Box, error) { textOffset := 2 legendWidth := 30 legendHeight := 20 + itemMaxHeight := 0 for _, item := range measureList { + if item.Height() > itemMaxHeight { + itemMaxHeight = item.Height() + } if opt.Orient == OrientVertical { height += item.Height() } else { @@ -170,6 +174,10 @@ func (l *legendPainter) Render() (Box, error) { } top, _ := strconv.Atoi(opt.Top) + if left < 0 { + left = 0 + } + x := int(left) y := int(top) + 10 x0 := x @@ -199,6 +207,10 @@ func (l *legendPainter) Render() (Box, error) { FillColor: color, StrokeColor: color, }) + if x0+measureList[index].Width() > p.Width() { + x0 = 0 + y0 += itemMaxHeight + } if opt.Align != AlignRight { x0 = drawIcon(y0, x0) x0 += textOffset From 0a3ac7096a30b41861faa8cb96bfa1bf84a7465c Mon Sep 17 00:00:00 2001 From: vicanso Date: Wed, 6 Jul 2022 20:44:52 +0800 Subject: [PATCH 045/116] refactor: adjust text render of axis --- axis.go | 8 +++++++- painter.go | 3 ++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/axis.go b/axis.go index 53b5362..17e8e9f 100644 --- a/axis.go +++ b/axis.go @@ -153,8 +153,14 @@ func (a *axisPainter) Render() (Box, error) { top.SetDrawingStyle(style).OverrideTextStyle(style) textMaxWidth, textMaxHeight := top.MeasureTextMaxWidthHeight(data) - textCount := ceilFloatToInt(float64(top.Width()) / float64(textMaxWidth)) + + textFillWidth := float64(textMaxWidth) * 1.3 + textCount := ceilFloatToInt(float64(top.Width()) / textFillWidth) unit := ceilFloatToInt(float64(dataCount) / float64(chart.MaxInt(textCount, opt.SplitNumber))) + // 偶数 + if unit%2 == 0 && dataCount%(unit+1) == 0 { + unit++ + } width := 0 height := 0 diff --git a/painter.go b/painter.go index 62a4378..0771288 100644 --- a/painter.go +++ b/painter.go @@ -653,8 +653,9 @@ func (p *Painter) MultiText(opt MultiTextOption) *Painter { } else { values = autoDivide(width, count) } + showIndex := opt.Unit / 2 for index, text := range opt.TextList { - if opt.Unit != 0 && index%opt.Unit != 0 { + if opt.Unit != 0 && index%opt.Unit != showIndex { continue } box := p.MeasureText(text) From c220b10ae600c68112792277c5a1103499964e38 Mon Sep 17 00:00:00 2001 From: vicanso Date: Thu, 7 Jul 2022 20:50:29 +0800 Subject: [PATCH 046/116] refactor: adjust label padding of axis --- axis.go | 3 ++- bar_chart_test.go | 2 +- chart_option_test.go | 2 +- echarts_test.go | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/axis.go b/axis.go index 17e8e9f..ebc6782 100644 --- a/axis.go +++ b/axis.go @@ -154,7 +154,8 @@ func (a *axisPainter) Render() (Box, error) { textMaxWidth, textMaxHeight := top.MeasureTextMaxWidthHeight(data) - textFillWidth := float64(textMaxWidth) * 1.3 + // 增加30px来计算文本展示区域 + textFillWidth := float64(textMaxWidth + 20) textCount := ceilFloatToInt(float64(top.Width()) / textFillWidth) unit := ceilFloatToInt(float64(dataCount) / float64(chart.MaxInt(textCount, opt.SplitNumber))) // 偶数 diff --git a/bar_chart_test.go b/bar_chart_test.go index 138b3ca..f1bd688 100644 --- a/bar_chart_test.go +++ b/bar_chart_test.go @@ -102,7 +102,7 @@ func TestBarChart(t *testing.T) { } return p.Bytes() }, - result: "\\n24020016012080400JanFebMarAprMayJunJulAugSepOctNovDec24.9723.225.676.7135.6162.232.6206.43.32.65.9926.428.770.7175.6182.248.718.862.3", + result: "\\n24020016012080400FebMayAugNov24.9723.225.676.7135.6162.232.6206.43.32.65.9926.428.770.7175.6182.248.718.862.3", }, } diff --git a/chart_option_test.go b/chart_option_test.go index 5e53e46..1238422 100644 --- a/chart_option_test.go +++ b/chart_option_test.go @@ -277,7 +277,7 @@ func TestBarRender(t *testing.T) { assert.Nil(err) data, err := p.Bytes() assert.Nil(err) - assert.Equal("\\nRainfallEvaporation24020016012080400JanFebMarAprMayJunJulAugSepOctNovDec162.22182.22.341.6248.07", string(data)) + assert.Equal("\\nRainfallEvaporation24020016012080400FebMayAugNov162.22182.22.341.6248.07", string(data)) } func TestHorizontalBarRender(t *testing.T) { diff --git a/echarts_test.go b/echarts_test.go index 4d50d9e..8deda2d 100644 --- a/echarts_test.go +++ b/echarts_test.go @@ -578,5 +578,5 @@ func TestRenderEChartsToSVG(t *testing.T) { ] }`) assert.Nil(err) - assert.Equal("\\nRainfallEvaporationRainfall vs EvaporationFake Data24020016012080400JanFebMarAprMayJunJulAugSepOctNovDec162.22182.22.341.6248.07", string(data)) + assert.Equal("\\nRainfallEvaporationRainfall vs EvaporationFake Data24020016012080400FebMayAugNov162.22182.22.341.6248.07", string(data)) } From 959377542e2a1fa1daaec7defdfd56195d3ea4d9 Mon Sep 17 00:00:00 2001 From: vicanso Date: Fri, 8 Jul 2022 21:11:47 +0800 Subject: [PATCH 047/116] fix: fix multi line label --- chart_option_test.go | 4 ++-- legend.go | 7 +++++-- line_chart_test.go | 4 ++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/chart_option_test.go b/chart_option_test.go index 1238422..0cdc2aa 100644 --- a/chart_option_test.go +++ b/chart_option_test.go @@ -204,7 +204,7 @@ func TestLineRender(t *testing.T) { assert.Nil(err) data, err := p.Bytes() assert.Nil(err) - assert.Equal("\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", string(data)) + assert.Equal("\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", string(data)) } func TestBarRender(t *testing.T) { @@ -277,7 +277,7 @@ func TestBarRender(t *testing.T) { assert.Nil(err) data, err := p.Bytes() assert.Nil(err) - assert.Equal("\\nRainfallEvaporation24020016012080400FebMayAugNov162.22182.22.341.6248.07", string(data)) + assert.Equal("\\nRainfallEvaporation24020016012080400FebMayAugNov162.22182.22.341.6248.07", string(data)) } func TestHorizontalBarRender(t *testing.T) { diff --git a/legend.go b/legend.go index d3b135e..820f1b5 100644 --- a/legend.go +++ b/legend.go @@ -146,6 +146,8 @@ func (l *legendPainter) Render() (Box, error) { width += item.Width() } } + // 增加padding + itemMaxHeight += 10 if opt.Orient == OrientVertical { width = maxTextWidth + textOffset + legendWidth height = offset * len(opt.Data) @@ -207,9 +209,10 @@ func (l *legendPainter) Render() (Box, error) { FillColor: color, StrokeColor: color, }) - if x0+measureList[index].Width() > p.Width() { + if x0+measureList[index].Width()+textOffset+offset+legendWidth > p.Width() { x0 = 0 - y0 += itemMaxHeight + y += itemMaxHeight + y0 = y } if opt.Align != AlignRight { x0 = drawIcon(y0, x0) diff --git a/line_chart_test.go b/line_chart_test.go index 856cdf3..ff80741 100644 --- a/line_chart_test.go +++ b/line_chart_test.go @@ -117,7 +117,7 @@ func TestLineChart(t *testing.T) { } return p.Bytes() }, - result: "\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", + result: "\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", }, { render: func(p *Painter) ([]byte, error) { @@ -201,7 +201,7 @@ func TestLineChart(t *testing.T) { } return p.Bytes() }, - result: "\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", + result: "\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", }, } From 805f4381a31f3f46c0a711623ed6f7b94ac98ffa Mon Sep 17 00:00:00 2001 From: vicanso Date: Mon, 11 Jul 2022 20:20:41 +0800 Subject: [PATCH 048/116] fix: fix multi line legend --- chart_option_test.go | 6 +++--- charts.go | 9 +++++++-- legend.go | 9 ++++++++- line_chart_test.go | 4 ++-- pie_chart_test.go | 2 +- 5 files changed, 21 insertions(+), 9 deletions(-) diff --git a/chart_option_test.go b/chart_option_test.go index 0cdc2aa..a025c25 100644 --- a/chart_option_test.go +++ b/chart_option_test.go @@ -204,7 +204,7 @@ func TestLineRender(t *testing.T) { assert.Nil(err) data, err := p.Bytes() assert.Nil(err) - assert.Equal("\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", string(data)) + assert.Equal("\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", string(data)) } func TestBarRender(t *testing.T) { @@ -277,7 +277,7 @@ func TestBarRender(t *testing.T) { assert.Nil(err) data, err := p.Bytes() assert.Nil(err) - assert.Equal("\\nRainfallEvaporation24020016012080400FebMayAugNov162.22182.22.341.6248.07", string(data)) + assert.Equal("\\nRainfallEvaporation24020016012080400FebMayAugNov162.22182.22.341.6248.07", string(data)) } func TestHorizontalBarRender(t *testing.T) { @@ -368,7 +368,7 @@ func TestPieRender(t *testing.T) { assert.Nil(err) data, err := p.Bytes() assert.Nil(err) - assert.Equal("\\nSearch EngineDirectEmailUnion AdsVideo AdsRainfall vs EvaporationFake DataSearch Engine: 33.3%Direct: 23.35%Email: 18.43%Union Ads: 15.37%Video Ads: 9.53%", string(data)) + assert.Equal("\\nSearch EngineDirectEmailUnion AdsVideo AdsRainfall vs EvaporationFake DataSearch Engine: 33.3%Direct: 23.35%Email: 18.43%Union Ads: 15.37%Video Ads: 9.53%", string(data)) } func TestRadarRender(t *testing.T) { diff --git a/charts.go b/charts.go index 6c1c92b..41802d9 100644 --- a/charts.go +++ b/charts.go @@ -25,6 +25,8 @@ package charts import ( "errors" "sort" + + "github.com/wcharczuk/go-chart/v2" ) const labelFontSize = 10 @@ -110,14 +112,16 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e p = p.Child(PainterPaddingOption(opt.Padding)) } + legendHeight := 0 if len(opt.LegendOption.Data) != 0 { if opt.LegendOption.Theme == nil { opt.LegendOption.Theme = opt.Theme } - _, err := NewLegendPainter(p, opt.LegendOption).Render() + legendResult, err := NewLegendPainter(p, opt.LegendOption).Render() if err != nil { return nil, err } + legendHeight = legendResult.Height() } // 如果有标题 @@ -131,9 +135,10 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e if err != nil { return nil, err } + p = p.Child(PainterPaddingOption(Box{ // 标题下留白 - Top: titleBox.Height() + 20, + Top: chart.MaxInt(legendHeight, titleBox.Height()) + 20, })) } diff --git a/legend.go b/legend.go index 820f1b5..4e2bc82 100644 --- a/legend.go +++ b/legend.go @@ -182,6 +182,7 @@ func (l *legendPainter) Render() (Box, error) { x := int(left) y := int(top) + 10 + startY := y x0 := x y0 := y @@ -203,13 +204,18 @@ func (l *legendPainter) Render() (Box, error) { } return left + legendWidth } + lastIndex := len(opt.Data) - 1 for index, text := range opt.Data { color := theme.GetSeriesColor(index) p.SetDrawingStyle(Style{ FillColor: color, StrokeColor: color, }) - if x0+measureList[index].Width()+textOffset+offset+legendWidth > p.Width() { + itemWidth := x0 + measureList[index].Width() + textOffset + offset + legendWidth + if lastIndex == index { + itemWidth = x0 + measureList[index].Width() + legendWidth + } + if itemWidth > p.Width() { x0 = 0 y += itemMaxHeight y0 = y @@ -231,6 +237,7 @@ func (l *legendPainter) Render() (Box, error) { x0 += offset y0 = y } + height = y0 - startY + 10 } return Box{ diff --git a/line_chart_test.go b/line_chart_test.go index ff80741..856cdf3 100644 --- a/line_chart_test.go +++ b/line_chart_test.go @@ -117,7 +117,7 @@ func TestLineChart(t *testing.T) { } return p.Bytes() }, - result: "\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", + result: "\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", }, { render: func(p *Painter) ([]byte, error) { @@ -201,7 +201,7 @@ func TestLineChart(t *testing.T) { } return p.Bytes() }, - result: "\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", + result: "\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", }, } diff --git a/pie_chart_test.go b/pie_chart_test.go index c373a7e..070fb03 100644 --- a/pie_chart_test.go +++ b/pie_chart_test.go @@ -78,7 +78,7 @@ func TestPieChart(t *testing.T) { } return p.Bytes() }, - result: "\\nSearch EngineDirectEmailUnion AdsVideo AdsRainfall vs EvaporationFake DataSearch Engine: 33.3%Direct: 23.35%Email: 18.43%Union Ads: 15.37%Video Ads: 9.53%", + result: "\\nSearch EngineDirectEmailUnion AdsVideo AdsRainfall vs EvaporationFake DataSearch Engine: 33.3%Direct: 23.35%Email: 18.43%Union Ads: 15.37%Video Ads: 9.53%", }, } for _, tt := range tests { From b5b2d37e875cf765e04ba2a312a8b1efdc6d5d03 Mon Sep 17 00:00:00 2001 From: vicanso Date: Mon, 11 Jul 2022 20:44:28 +0800 Subject: [PATCH 049/116] fix: fix axis boundary gap, #13 --- painter.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/painter.go b/painter.go index 0771288..1a954e2 100644 --- a/painter.go +++ b/painter.go @@ -637,12 +637,15 @@ func (p *Painter) MultiText(opt MultiTextOption) *Painter { } count := len(opt.TextList) positionCenter := true + showIndex := opt.Unit / 2 if containsString([]string{ PositionLeft, PositionTop, }, opt.Position) { positionCenter = false count-- + // 非居中 + showIndex = 0 } width := p.Width() height := p.Height() @@ -653,7 +656,6 @@ func (p *Painter) MultiText(opt MultiTextOption) *Painter { } else { values = autoDivide(width, count) } - showIndex := opt.Unit / 2 for index, text := range opt.TextList { if opt.Unit != 0 && index%opt.Unit != showIndex { continue From 3af0d4d4450652f132a8f5482aa30e0eb39ba9d4 Mon Sep 17 00:00:00 2001 From: vicanso Date: Thu, 14 Jul 2022 20:14:32 +0800 Subject: [PATCH 050/116] fix: fix pie chart legend --- chart_option_test.go | 2 +- charts.go | 7 ++++++- pie_chart_test.go | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/chart_option_test.go b/chart_option_test.go index a025c25..1238422 100644 --- a/chart_option_test.go +++ b/chart_option_test.go @@ -368,7 +368,7 @@ func TestPieRender(t *testing.T) { assert.Nil(err) data, err := p.Bytes() assert.Nil(err) - assert.Equal("\\nSearch EngineDirectEmailUnion AdsVideo AdsRainfall vs EvaporationFake DataSearch Engine: 33.3%Direct: 23.35%Email: 18.43%Union Ads: 15.37%Video Ads: 9.53%", string(data)) + assert.Equal("\\nSearch EngineDirectEmailUnion AdsVideo AdsRainfall vs EvaporationFake DataSearch Engine: 33.3%Direct: 23.35%Email: 18.43%Union Ads: 15.37%Video Ads: 9.53%", string(data)) } func TestRadarRender(t *testing.T) { diff --git a/charts.go b/charts.go index 41802d9..36bb17e 100644 --- a/charts.go +++ b/charts.go @@ -136,9 +136,14 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e return nil, err } + top := chart.MaxInt(legendHeight, titleBox.Height()) + // 如果是垂直方式,则不计算legend高度 + if opt.LegendOption.Orient == OrientVertical { + top = titleBox.Height() + } p = p.Child(PainterPaddingOption(Box{ // 标题下留白 - Top: chart.MaxInt(legendHeight, titleBox.Height()) + 20, + Top: top + 20, })) } diff --git a/pie_chart_test.go b/pie_chart_test.go index 070fb03..c373a7e 100644 --- a/pie_chart_test.go +++ b/pie_chart_test.go @@ -78,7 +78,7 @@ func TestPieChart(t *testing.T) { } return p.Bytes() }, - result: "\\nSearch EngineDirectEmailUnion AdsVideo AdsRainfall vs EvaporationFake DataSearch Engine: 33.3%Direct: 23.35%Email: 18.43%Union Ads: 15.37%Video Ads: 9.53%", + result: "\\nSearch EngineDirectEmailUnion AdsVideo AdsRainfall vs EvaporationFake DataSearch Engine: 33.3%Direct: 23.35%Email: 18.43%Union Ads: 15.37%Video Ads: 9.53%", }, } for _, tt := range tests { From 8740c55a1a90f6f895f6601a6afd8f3c4fcf0cbd Mon Sep 17 00:00:00 2001 From: vicanso Date: Tue, 19 Jul 2022 20:12:31 +0800 Subject: [PATCH 051/116] feat: support padding for legend --- examples/line_chart/main.go | 8 +++++++- legend.go | 12 ++++++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/examples/line_chart/main.go b/examples/line_chart/main.go index 45ff894..a941bca 100644 --- a/examples/line_chart/main.go +++ b/examples/line_chart/main.go @@ -89,7 +89,13 @@ func main() { "Video Ads", "Direct", "Search Engine", - }, charts.PositionCenter), + }, "50"), + func(opt *charts.ChartOption) { + opt.Legend.Padding = charts.Box{ + Top: 5, + Bottom: 10, + } + }, ) if err != nil { diff --git a/legend.go b/legend.go index 4e2bc82..8f21afb 100644 --- a/legend.go +++ b/legend.go @@ -59,6 +59,8 @@ type LegendOption struct { FontColor Color // The flag for show legend, set this to *false will hide legend Show *bool + // The padding of legend + Padding Box } // NewLegendOption returns a legend option @@ -111,9 +113,11 @@ func (l *legendPainter) Render() (Box, error) { if opt.Left == "" { opt.Left = PositionCenter } - p := l.p.Child(PainterPaddingOption(Box{ - Top: 5, - })) + padding := opt.Padding + if padding.IsZero() { + padding.Top = 5 + } + p := l.p.Child(PainterPaddingOption(padding)) p.SetTextStyle(Style{ FontSize: opt.FontSize, FontColor: opt.FontColor, @@ -242,6 +246,6 @@ func (l *legendPainter) Render() (Box, error) { return Box{ Right: width, - Bottom: height, + Bottom: height + padding.Bottom + padding.Top, }, nil } From 3d20bea84663801858ac28067bdbe2f536bb7408 Mon Sep 17 00:00:00 2001 From: vicanso Date: Fri, 22 Jul 2022 20:25:12 +0800 Subject: [PATCH 052/116] refactor: remove unused code --- title.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/title.go b/title.go index 5cdd161..74ab4f9 100644 --- a/title.go +++ b/title.go @@ -36,10 +36,6 @@ type TitleOption struct { Text string // Subtitle text, support \n for new line Subtext string - // // Title style - // Style Style - // // Subtitle style - // SubtextStyle Style // Distance between title component and the left side of the container. // It can be pixel value: 20, percentage value: 20%, // or position value: right, center. From cac6fd03d31477505f3c0e2fdeb62a546e0c8f3b Mon Sep 17 00:00:00 2001 From: vicanso Date: Tue, 26 Jul 2022 20:44:50 +0800 Subject: [PATCH 053/116] fix: fix unit count of xasix --- axis.go | 7 +++++-- chart_option.go | 5 ++++- legend.go | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/axis.go b/axis.go index ebc6782..8e5bfda 100644 --- a/axis.go +++ b/axis.go @@ -156,8 +156,11 @@ func (a *axisPainter) Render() (Box, error) { // 增加30px来计算文本展示区域 textFillWidth := float64(textMaxWidth + 20) - textCount := ceilFloatToInt(float64(top.Width()) / textFillWidth) - unit := ceilFloatToInt(float64(dataCount) / float64(chart.MaxInt(textCount, opt.SplitNumber))) + // 根据文本宽度计算较为符合的展示项 + fitTextCount := ceilFloatToInt(float64(top.Width()) / textFillWidth) + + unit := ceilFloatToInt(float64(dataCount) / float64(fitTextCount)) + unit = chart.MaxInt(unit, opt.SplitNumber) // 偶数 if unit%2 == 0 && dataCount%(unit+1) == 0 { unit++ diff --git a/chart_option.go b/chart_option.go index 39de686..41fda46 100644 --- a/chart_option.go +++ b/chart_option.go @@ -108,9 +108,12 @@ func TitleOptionFunc(title TitleOption) OptionFunc { } // TitleTextOptionFunc set title text of chart -func TitleTextOptionFunc(text string) OptionFunc { +func TitleTextOptionFunc(text string, subtext ...string) OptionFunc { return func(opt *ChartOption) { opt.Title.Text = text + if len(subtext) != 0 { + opt.Title.Subtext = subtext[0] + } } } diff --git a/legend.go b/legend.go index 8f21afb..035642c 100644 --- a/legend.go +++ b/legend.go @@ -232,7 +232,7 @@ func (l *legendPainter) Render() (Box, error) { x0 += measureList[index].Width() if opt.Align == AlignRight { x0 += textOffset - x0 = drawIcon(0, x0) + x0 = drawIcon(y0, x0) } if opt.Orient == OrientVertical { y0 += offset From 1713bc283f6e742cf45cbed65cce010509945130 Mon Sep 17 00:00:00 2001 From: vicanso Date: Tue, 26 Jul 2022 20:45:04 +0800 Subject: [PATCH 054/116] docs: add doc --- start_zh.md | 254 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 254 insertions(+) create mode 100644 start_zh.md diff --git a/start_zh.md b/start_zh.md new file mode 100644 index 0000000..ee8359c --- /dev/null +++ b/start_zh.md @@ -0,0 +1,254 @@ +# go-charts + +`go-charts`主要分为了下几个模块: + +- `标题`:图表的标题,包括主副标题,位置为图表的顶部 +- `图例`:图表的图例列表,用于标识每个图例对应的颜色与名称信息,默认为图表的顶部,可自定义位置 +- `X轴`:图表的x轴,用于折线图、柱状图中,表示每个点对应的时间,位置图表的底部 +- `Y轴`:图表的y轴,用于折线图、柱状图中,最多可使用两组y轴(一左一右),默认位置图表的左侧 +- `内容`: 图表的内容,折线图、柱状图、饼图等,在图表的中间区域 + +## 标题 + +### 常用设置 + +标题一般仅需要设置主副标题即可,其它的属性均会设置默认值,常用的方式是使用`TitleTextOptionFunc`设置,其中副标题为可选值,方式如下: + +```go + charts.TitleTextOptionFunc("Text", "Subtext"), +``` + +### 个性化设置 + +```go +func(opt *charts.ChartOption) { + opt.Title = charts.TitleOption{ + // 主标题 + Text: "Text", + // 副标题 + Subtext: "Subtext", + // 标题左侧位置,可设置为"center","right",数值("20")或百份比("20%") + Left: charts.PositionRight, + // 标题顶部位置,只可调为数值 + Top: "20", + // 主标题文字大小 + FontSize: 14, + // 副标题文字大小 + SubtextFontSize: 12, + // 主标题字体颜色 + FontColor: charts.Color{ + R: 100, + G: 100, + B: 100, + A: 255, + }, + // 副标题字体影响 + SubtextFontColor: charts.Color{ + R: 200, + G: 200, + B: 200, + A: 255, + }, + } +}, +``` + +### 部分属性个性化设置 + +```go +charts.TitleTextOptionFunc("Text", "Subtext"), +func(opt *charts.ChartOption) { + // 修改top的值 + opt.Title.Top = "20" +}, +``` + +## 图例 + +### 常用设置 + +图例组件与图表中的数据一一对应,常用仅设置其名称及左侧的值即可(可选),方式如下: + + +```go +charts.LegendLabelsOptionFunc([]string{ + "Email", + "Union Ads", + "Video Ads", + "Direct", + "Search Engine", +}, "50"), +``` + +### 个性化设置 + +```go +func(opt *charts.ChartOption) { + opt.Legend = charts.LegendOption{ + // 图例名称 + Data: []string{ + "Email", + "Union Ads", + "Video Ads", + "Direct", + "Search Engine", + }, + // 图例左侧位置,可设置为"center","right",数值("20")或百份比("20%") + // 如果示例有多行,只影响第一行,而且对于多行的示例,设置"center", "right"无效 + Left: "50", + // 图例顶部位置,只可调为数值 + Top: "10", + // 图例图标的位置,默认为左侧,只允许左或右 + Align: charts.AlignRight, + // 图例排列方式,默认为水平,只允许水平或垂直 + Orient: charts.OrientVertical, + // 图标类型,提供"rect"与"lineDot"两种类型 + Icon: charts.IconRect, + // 字体大小 + FontSize: 14, + // 字体颜色 + FontColor: charts.Color{ + R: 150, + G: 150, + B: 150, + A: 255, + }, + // 是否展示,如果不需要展示则设置 + // Show: charts.FalseFlag(), + // 图例区域的padding值 + Padding: charts.Box{ + Top: 10, + Left: 10, + }, + } +}, +``` + +### 部分属性个性化设置 + +```go +charts.LegendLabelsOptionFunc([]string{ + "Email", + "Union Ads", + "Video Ads", + "Direct", + "Search Engine", +}, "50"), +func(opt *charts.ChartOption) { + opt.Legend.Top = "10" +}, +``` + +## X轴 + +### 常用设置 + +图表中X轴的展示,常用的设置方式是指定数组即可: + +```go +charts.XAxisDataOptionFunc([]string{ + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun", +}), +``` + +### 个性化设置 + +```go +func(opt *charts.ChartOption) { + opt.XAxis = charts.XAxisOption{ + // X轴内容 + Data: []string{ + "01", + "02", + "03", + "04", + "05", + "06", + "07", + "08", + "09", + }, + // 如果数据点不居中,则设置为false + BoundaryGap: charts.FalseFlag(), + // 字体大小 + FontSize: 14, + // 是否展示,如果不需要展示则设置 + // Show: charts.FalseFlag(), + // 会根据文本内容以及此值选择适合的分块大小,一般不需要设置 + // SplitNumber: 3, + // 线条颜色 + StrokeColor: charts.Color{ + R: 200, + G: 200, + B: 200, + A: 255, + }, + // 文字颜色 + FontColor: charts.Color{ + R: 100, + G: 100, + B: 100, + A: 255, + }, + } +}, +``` + +### 部分属性个性化设置 + +```go +charts.XAxisDataOptionFunc([]string{ + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun", +}), +func(opt *charts.ChartOption) { + opt.XAxis.FontColor = charts.Color{ + R: 100, + G: 100, + B: 100, + A: 255, + }, +}, +``` + +## Y轴 + +图表中的y轴展示的相关数据会根据图表中的数据自动生成适合的值,如果需要自定义,则可自定义以下部分数据: + +```go +func(opt *charts.ChartOption) { + opt.YAxisOptions = []charts.YAxisOption{ + { + // 字体大小 + FontSize: 16, + // 字体颜色 + FontColor: charts.Color{ + R: 100, + G: 100, + B: 100, + A: 255, + }, + // 内容,{value}会替换为对应的值 + Formatter: "{value} ml", + // Y轴颜色,如果设置此值,会覆盖font color + Color: charts.Color{ + R: 255, + G: 0, + B: 0, + A: 255, + }, + }, + } +}, +``` From e095223705464e4724ca3fb29512def0daecde55 Mon Sep 17 00:00:00 2001 From: vicanso Date: Wed, 27 Jul 2022 20:27:49 +0800 Subject: [PATCH 055/116] fix: fix font setting for title, #15 --- .gitignore | 1 + chart_option.go | 3 +++ examples/chinese/main.go | 5 +++-- theme.go | 39 +++++++++++++++++++++++++++++++++++++-- 4 files changed, 44 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 2e33342..4a7b0d9 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ *.png *.svg tmp +NotoSansSC.ttf diff --git a/chart_option.go b/chart_option.go index 41fda46..cb3bd3f 100644 --- a/chart_option.go +++ b/chart_option.go @@ -259,6 +259,9 @@ func (o *ChartOption) fillDefault() { if o.font == nil { o.font, _ = chart.GetDefaultFont() + } else { + // 如果指定了字体,则设置主题的字体 + t.SetFont(o.font) } if o.BackgroundColor.IsZero() { o.BackgroundColor = t.GetBackgroundColor() diff --git a/examples/chinese/main.go b/examples/chinese/main.go index bb7cc00..9068a08 100644 --- a/examples/chinese/main.go +++ b/examples/chinese/main.go @@ -25,7 +25,8 @@ func writeFile(buf []byte) error { func main() { // 字体文件需要自行下载 - buf, err := ioutil.ReadFile("../NotoSansSC.ttf") + // https://github.com/googlefonts/noto-cjk + buf, err := ioutil.ReadFile("./NotoSansSC.ttf") if err != nil { panic(err) } @@ -83,7 +84,7 @@ func main() { } p, err := charts.LineRender( values, - charts.TitleTextOptionFunc("Line"), + charts.TitleTextOptionFunc("测试"), charts.FontFamilyOptionFunc("noto"), charts.XAxisDataOptionFunc([]string{ "星期一", diff --git a/theme.go b/theme.go index 31c3bf8..8068687 100644 --- a/theme.go +++ b/theme.go @@ -36,12 +36,19 @@ const ThemeAnt = "ant" type ColorPalette interface { IsDark() bool GetAxisStrokeColor() Color + SetAxisStrokeColor(Color) GetAxisSplitLineColor() Color + SetAxisSplitLineColor(Color) GetSeriesColor(int) Color + SetSeriesColor([]Color) GetBackgroundColor() Color + SetBackgroundColor(Color) GetTextColor() Color + SetTextColor(Color) GetFontSize() float64 + SetFontSize(float64) GetFont() *truetype.Font + SetFont(*truetype.Font) } type themeColorPalette struct { @@ -64,7 +71,7 @@ type ThemeOption struct { SeriesColors []Color } -var palettes = map[string]ColorPalette{} +var palettes = map[string]*themeColorPalette{} const defaultFontSize = 12.0 @@ -241,7 +248,8 @@ func NewTheme(name string) ColorPalette { if !ok { p = palettes[ThemeLight] } - return p + clone := *p + return &clone } func (t *themeColorPalette) IsDark() bool { @@ -252,23 +260,42 @@ func (t *themeColorPalette) GetAxisStrokeColor() Color { return t.axisStrokeColor } +func (t *themeColorPalette) SetAxisStrokeColor(c Color) { + t.axisStrokeColor = c +} + func (t *themeColorPalette) GetAxisSplitLineColor() Color { return t.axisSplitLineColor } +func (t *themeColorPalette) SetAxisSplitLineColor(c Color) { + t.axisSplitLineColor = c +} + func (t *themeColorPalette) GetSeriesColor(index int) Color { colors := t.seriesColors return colors[index%len(colors)] } +func (t *themeColorPalette) SetSeriesColor(colors []Color) { + t.seriesColors = colors +} func (t *themeColorPalette) GetBackgroundColor() Color { return t.backgroundColor } +func (t *themeColorPalette) SetBackgroundColor(c Color) { + t.backgroundColor = c +} + func (t *themeColorPalette) GetTextColor() Color { return t.textColor } +func (t *themeColorPalette) SetTextColor(c Color) { + t.textColor = c +} + func (t *themeColorPalette) GetFontSize() float64 { if t.fontSize != 0 { return t.fontSize @@ -276,6 +303,10 @@ func (t *themeColorPalette) GetFontSize() float64 { return defaultFontSize } +func (t *themeColorPalette) SetFontSize(fontSize float64) { + t.fontSize = fontSize +} + func (t *themeColorPalette) GetFont() *truetype.Font { if t.font != nil { return t.font @@ -283,3 +314,7 @@ func (t *themeColorPalette) GetFont() *truetype.Font { f, _ := chart.GetDefaultFont() return f } + +func (t *themeColorPalette) SetFont(f *truetype.Font) { + t.font = f +} From 817fceff73798bba9450192a3dfa844319d08e8d Mon Sep 17 00:00:00 2001 From: vicanso Date: Wed, 27 Jul 2022 20:32:31 +0800 Subject: [PATCH 056/116] feat: support hide symbol of line chart --- chart_option.go | 2 ++ charts.go | 7 ++++--- examples/line_chart/main.go | 1 + line_chart.go | 6 +++++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/chart_option.go b/chart_option.go index cb3bd3f..71e9dfc 100644 --- a/chart_option.go +++ b/chart_option.go @@ -62,6 +62,8 @@ type ChartOption struct { RadarIndicators []RadarIndicator // The background color of chart BackgroundColor Color + // The flag for show symbol of line, set this to *false will hide symbol + SymbolShow *bool // The child charts Children []ChartOption } diff --git a/charts.go b/charts.go index 36bb17e..92a7e54 100644 --- a/charts.go +++ b/charts.go @@ -377,9 +377,10 @@ func Render(opt ChartOption, opts ...OptionFunc) (*Painter, error) { if len(lineSeriesList) != 0 { handler.Add(func() error { _, err := NewLineChart(p, LineChartOption{ - Theme: opt.theme, - Font: opt.font, - XAxis: opt.XAxis, + Theme: opt.theme, + Font: opt.font, + XAxis: opt.XAxis, + SymbolShow: opt.SymbolShow, }).render(renderResult, lineSeriesList) return err }) diff --git a/examples/line_chart/main.go b/examples/line_chart/main.go index a941bca..5edf65b 100644 --- a/examples/line_chart/main.go +++ b/examples/line_chart/main.go @@ -95,6 +95,7 @@ func main() { Top: 5, Bottom: 10, } + opt.SymbolShow = charts.FalseFlag() }, ) diff --git a/line_chart.go b/line_chart.go index 0770447..dee122f 100644 --- a/line_chart.go +++ b/line_chart.go @@ -60,6 +60,8 @@ type LineChartOption struct { Title TitleOption // The legend option Legend LegendOption + // The flag for show symbol of line, set this to *false will hide symbol + SymbolShow *bool // background is filled backgroundIsFilled bool } @@ -123,7 +125,9 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) ( } drawingStyle.StrokeWidth = 1 seriesPainter.SetDrawingStyle(drawingStyle) - seriesPainter.Dots(points) + if !isFalse(opt.SymbolShow) { + seriesPainter.Dots(points) + } markPointPainter.Add(markPointRenderOption{ FillColor: seriesColor, Font: opt.Font, From e530adccb66738e41d8eb9c7d042e38d3221f869 Mon Sep 17 00:00:00 2001 From: vicanso Date: Thu, 28 Jul 2022 20:49:00 +0800 Subject: [PATCH 057/116] feat: support stroke width of line chart --- chart_option.go | 2 ++ charts.go | 9 +++++---- examples/line_chart/main.go | 1 + line_chart.go | 8 +++++++- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/chart_option.go b/chart_option.go index 71e9dfc..58001bd 100644 --- a/chart_option.go +++ b/chart_option.go @@ -64,6 +64,8 @@ type ChartOption struct { BackgroundColor Color // The flag for show symbol of line, set this to *false will hide symbol SymbolShow *bool + // The stroke width of line chart + LineStrokeWidth float64 // The child charts Children []ChartOption } diff --git a/charts.go b/charts.go index 92a7e54..d65f3c9 100644 --- a/charts.go +++ b/charts.go @@ -377,10 +377,11 @@ func Render(opt ChartOption, opts ...OptionFunc) (*Painter, error) { if len(lineSeriesList) != 0 { handler.Add(func() error { _, err := NewLineChart(p, LineChartOption{ - Theme: opt.theme, - Font: opt.font, - XAxis: opt.XAxis, - SymbolShow: opt.SymbolShow, + Theme: opt.theme, + Font: opt.font, + XAxis: opt.XAxis, + SymbolShow: opt.SymbolShow, + StrokeWidth: opt.LineStrokeWidth, }).render(renderResult, lineSeriesList) return err }) diff --git a/examples/line_chart/main.go b/examples/line_chart/main.go index 5edf65b..36eabee 100644 --- a/examples/line_chart/main.go +++ b/examples/line_chart/main.go @@ -96,6 +96,7 @@ func main() { Bottom: 10, } opt.SymbolShow = charts.FalseFlag() + opt.LineStrokeWidth = 1 }, ) diff --git a/line_chart.go b/line_chart.go index dee122f..3942d70 100644 --- a/line_chart.go +++ b/line_chart.go @@ -62,6 +62,8 @@ type LineChartOption struct { Legend LegendOption // The flag for show symbol of line, set this to *false will hide symbol SymbolShow *bool + // The stroke width of line + StrokeWidth float64 // background is filled backgroundIsFilled bool } @@ -95,12 +97,16 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) ( markPointPainter, markLinePainter, } + strokeWidth := opt.StrokeWidth + if strokeWidth == 0 { + strokeWidth = defaultStrokeWidth + } for index := range seriesList { series := seriesList[index] seriesColor := opt.Theme.GetSeriesColor(series.index) drawingStyle := Style{ StrokeColor: seriesColor, - StrokeWidth: defaultStrokeWidth, + StrokeWidth: strokeWidth, } seriesPainter.SetDrawingStyle(drawingStyle) From 550b9874d23dc966e54248455a6e639c29affd26 Mon Sep 17 00:00:00 2001 From: vicanso Date: Fri, 29 Jul 2022 20:42:13 +0800 Subject: [PATCH 058/116] refactor: remove unused path --- axis.go | 5 ++++- axis_test.go | 4 ++-- bar_chart_test.go | 2 +- chart_option_test.go | 12 ++++++------ charts.go | 5 ++--- echarts_test.go | 2 +- horizontal_bar_chart_test.go | 2 +- line_chart_test.go | 4 ++-- 8 files changed, 19 insertions(+), 17 deletions(-) diff --git a/axis.go b/axis.go index 8e5bfda..3c0484c 100644 --- a/axis.go +++ b/axis.go @@ -265,6 +265,7 @@ func (a *axisPainter) Render() (Box, error) { // 显示辅助线 if opt.SplitLineShow { style.StrokeColor = opt.SplitLineColor + style.StrokeWidth = 1 top.OverrideDrawingStyle(style) if isVertical { x0 := p.Width() @@ -273,7 +274,9 @@ func (a *axisPainter) Render() (Box, error) { x0 = 0 x1 = top.Width() - p.Width() } - for _, y := range autoDivide(height, tickCount) { + yValues := autoDivide(height, tickCount) + yValues = yValues[0 : len(yValues)-1] + for _, y := range yValues { top.LineStroke([]Point{ { X: x0, diff --git a/axis_test.go b/axis_test.go index 17fe8d6..d0cff41 100644 --- a/axis_test.go +++ b/axis_test.go @@ -113,7 +113,7 @@ func TestAxis(t *testing.T) { }).Render() return p.Bytes() }, - result: "\\nMonTueWedThuFriSatSun", + result: "\\nMonTueWedThuFriSatSun", }, // 右侧 { @@ -135,7 +135,7 @@ func TestAxis(t *testing.T) { }).Render() return p.Bytes() }, - result: "\\nMonTueWedThuFriSatSun", + result: "\\nMonTueWedThuFriSatSun", }, // 顶部 { diff --git a/bar_chart_test.go b/bar_chart_test.go index f1bd688..bee0583 100644 --- a/bar_chart_test.go +++ b/bar_chart_test.go @@ -102,7 +102,7 @@ func TestBarChart(t *testing.T) { } return p.Bytes() }, - result: "\\n24020016012080400FebMayAugNov24.9723.225.676.7135.6162.232.6206.43.32.65.9926.428.770.7175.6182.248.718.862.3", + result: "\\n24020016012080400FebMayAugNov24.9723.225.676.7135.6162.232.6206.43.32.65.9926.428.770.7175.6182.248.718.862.3", }, } diff --git a/chart_option_test.go b/chart_option_test.go index 1238422..6f331b3 100644 --- a/chart_option_test.go +++ b/chart_option_test.go @@ -204,7 +204,7 @@ func TestLineRender(t *testing.T) { assert.Nil(err) data, err := p.Bytes() assert.Nil(err) - assert.Equal("\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", string(data)) + assert.Equal("\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", string(data)) } func TestBarRender(t *testing.T) { @@ -277,7 +277,7 @@ func TestBarRender(t *testing.T) { assert.Nil(err) data, err := p.Bytes() assert.Nil(err) - assert.Equal("\\nRainfallEvaporation24020016012080400FebMayAugNov162.22182.22.341.6248.07", string(data)) + assert.Equal("\\nRainfallEvaporation24020016012080400FebMayAugNov162.22182.22.341.6248.07", string(data)) } func TestHorizontalBarRender(t *testing.T) { @@ -326,7 +326,7 @@ func TestHorizontalBarRender(t *testing.T) { assert.Nil(err) data, err := p.Bytes() assert.Nil(err) - assert.Equal("\\n20112012World PopulationWorldChinaIndiaUSAIndonesiaBrazil0122.28k244.56k366.84k489.12k611.4k733.68k", string(data)) + assert.Equal("\\n20112012World PopulationWorldChinaIndiaUSAIndonesiaBrazil0122.28k244.56k366.84k489.12k611.4k733.68k", string(data)) } func TestPieRender(t *testing.T) { @@ -368,7 +368,7 @@ func TestPieRender(t *testing.T) { assert.Nil(err) data, err := p.Bytes() assert.Nil(err) - assert.Equal("\\nSearch EngineDirectEmailUnion AdsVideo AdsRainfall vs EvaporationFake DataSearch Engine: 33.3%Direct: 23.35%Email: 18.43%Union Ads: 15.37%Video Ads: 9.53%", string(data)) + assert.Equal("\\nSearch EngineDirectEmailUnion AdsVideo AdsRainfall vs EvaporationFake DataSearch Engine: 33.3%Direct: 23.35%Email: 18.43%Union Ads: 15.37%Video Ads: 9.53%", string(data)) } func TestRadarRender(t *testing.T) { @@ -419,7 +419,7 @@ func TestRadarRender(t *testing.T) { assert.Nil(err) data, err := p.Bytes() assert.Nil(err) - assert.Equal("\\nAllocated BudgetActual SpendingBasic Radar ChartSalesAdministrationInformation TechnologyCustomer SupportDevelopmentMarketing", string(data)) + assert.Equal("\\nAllocated BudgetActual SpendingBasic Radar ChartSalesAdministrationInformation TechnologyCustomer SupportDevelopmentMarketing", string(data)) } func TestFunnelRender(t *testing.T) { @@ -447,5 +447,5 @@ func TestFunnelRender(t *testing.T) { assert.Nil(err) data, err := p.Bytes() assert.Nil(err) - assert.Equal("\\nShowClickVisitInquiryOrderFunnelShow(100%)Click(80%)Visit(60%)Inquiry(40%)Order(20%)", string(data)) + assert.Equal("\\nShowClickVisitInquiryOrderFunnelShow(100%)Click(80%)Visit(60%)Inquiry(40%)Order(20%)", string(data)) } diff --git a/charts.go b/charts.go index d65f3c9..185e638 100644 --- a/charts.go +++ b/charts.go @@ -316,9 +316,8 @@ func Render(opt ChartOption, opts ...OptionFunc) (*Painter, error) { TitleOption: opt.Title, LegendOption: opt.Legend, axisReversed: axisReversed, - } - if isChild { - renderOpt.backgroundIsFilled = true + // 前置已设置背景色 + backgroundIsFilled: true, } if len(pieSeriesList) != 0 || len(radarSeriesList) != 0 || diff --git a/echarts_test.go b/echarts_test.go index 8deda2d..5c2dbad 100644 --- a/echarts_test.go +++ b/echarts_test.go @@ -578,5 +578,5 @@ func TestRenderEChartsToSVG(t *testing.T) { ] }`) assert.Nil(err) - assert.Equal("\\nRainfallEvaporationRainfall vs EvaporationFake Data24020016012080400FebMayAugNov162.22182.22.341.6248.07", string(data)) + assert.Equal("\\nRainfallEvaporationRainfall vs EvaporationFake Data24020016012080400FebMayAugNov162.22182.22.341.6248.07", string(data)) } diff --git a/horizontal_bar_chart_test.go b/horizontal_bar_chart_test.go index 5555df6..e078c4a 100644 --- a/horizontal_bar_chart_test.go +++ b/horizontal_bar_chart_test.go @@ -83,7 +83,7 @@ func TestHorizontalBarChart(t *testing.T) { } return p.Bytes() }, - result: "\\n20112012World PopulationWorldChinaIndiaUSAIndonesiaBrazil0122.28k244.56k366.84k489.12k611.4k733.68k", + result: "\\n20112012World PopulationWorldChinaIndiaUSAIndonesiaBrazil0122.28k244.56k366.84k489.12k611.4k733.68k", }, } for _, tt := range tests { diff --git a/line_chart_test.go b/line_chart_test.go index 856cdf3..e169f90 100644 --- a/line_chart_test.go +++ b/line_chart_test.go @@ -117,7 +117,7 @@ func TestLineChart(t *testing.T) { } return p.Bytes() }, - result: "\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", + result: "\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", }, { render: func(p *Painter) ([]byte, error) { @@ -201,7 +201,7 @@ func TestLineChart(t *testing.T) { } return p.Bytes() }, - result: "\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", + result: "\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", }, } From 93e03856cac44d574178f85f70fff4d4bc2ac1b3 Mon Sep 17 00:00:00 2001 From: vicanso Date: Wed, 10 Aug 2022 20:39:14 +0800 Subject: [PATCH 059/116] fix: fix NaN of radar chart, #17 --- radar_chart.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/radar_chart.go b/radar_chart.go index eab70d5..429850d 100644 --- a/radar_chart.go +++ b/radar_chart.go @@ -200,7 +200,11 @@ func (r *radarChart) render(result *defaultRenderResult, seriesList SeriesList) continue } indicator := indicators[j] - percent := (item.Value - indicator.Min) / (indicator.Max - indicator.Min) + var percent float64 + offset := indicator.Max - indicator.Min + if offset > 0 { + percent = (item.Value - indicator.Min) / offset + } r := percent * radius p := getPolygonPoint(center, r, angles[j]) linePoints = append(linePoints, p) From dc1a89d3ff8937afc58a4f90a6c935d11aa859ab Mon Sep 17 00:00:00 2001 From: vicanso Date: Thu, 25 Aug 2022 20:19:05 +0800 Subject: [PATCH 060/116] feat: support fill area of line chart --- .gitignore | 1 + chart_option.go | 2 + charts.go | 1 + examples/area_line_chart/main.go | 74 ++++++++++++++++++++++++++++++++ examples/charts/main.go | 31 +++++++++++++ line_chart.go | 22 +++++++++- 6 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 examples/area_line_chart/main.go diff --git a/.gitignore b/.gitignore index 4a7b0d9..57206ee 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ *.svg tmp NotoSansSC.ttf +.vscode \ No newline at end of file diff --git a/chart_option.go b/chart_option.go index 58001bd..93b81ba 100644 --- a/chart_option.go +++ b/chart_option.go @@ -66,6 +66,8 @@ type ChartOption struct { SymbolShow *bool // The stroke width of line chart LineStrokeWidth float64 + // Fill the area of line chart + FillArea bool // The child charts Children []ChartOption } diff --git a/charts.go b/charts.go index 185e638..849f0c7 100644 --- a/charts.go +++ b/charts.go @@ -381,6 +381,7 @@ func Render(opt ChartOption, opts ...OptionFunc) (*Painter, error) { XAxis: opt.XAxis, SymbolShow: opt.SymbolShow, StrokeWidth: opt.LineStrokeWidth, + FillArea: opt.FillArea, }).render(renderResult, lineSeriesList) return err }) diff --git a/examples/area_line_chart/main.go b/examples/area_line_chart/main.go new file mode 100644 index 0000000..7a84df0 --- /dev/null +++ b/examples/area_line_chart/main.go @@ -0,0 +1,74 @@ +package main + +import ( + "io/ioutil" + "os" + "path/filepath" + + "github.com/vicanso/go-charts/v2" +) + +func writeFile(buf []byte) error { + tmpPath := "./tmp" + err := os.MkdirAll(tmpPath, 0700) + if err != nil { + return err + } + + file := filepath.Join(tmpPath, "area-line-chart.png") + err = ioutil.WriteFile(file, buf, 0600) + if err != nil { + return err + } + return nil +} + +func main() { + values := [][]float64{ + { + 120, + 132, + 101, + 134, + 90, + 230, + 210, + }, + } + p, err := charts.LineRender( + values, + charts.TitleTextOptionFunc("Line"), + charts.XAxisDataOptionFunc([]string{ + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun", + }), + charts.LegendLabelsOptionFunc([]string{ + "Email", + }, "50"), + func(opt *charts.ChartOption) { + opt.Legend.Padding = charts.Box{ + Top: 5, + Bottom: 10, + } + opt.FillArea = true + }, + ) + + if err != nil { + panic(err) + } + + buf, err := p.Bytes() + if err != nil { + panic(err) + } + err = writeFile(buf) + if err != nil { + panic(err) + } +} diff --git a/examples/charts/main.go b/examples/charts/main.go index 7b14919..c3bb486 100644 --- a/examples/charts/main.go +++ b/examples/charts/main.go @@ -2,6 +2,7 @@ package main import ( "bytes" + "fmt" "net/http" "strconv" @@ -261,6 +262,35 @@ func indexHandler(w http.ResponseWriter, req *http.Request) { }, }, }, + { + Title: charts.TitleOption{ + Text: "Line Area", + }, + Legend: charts.NewLegendOption([]string{ + "Email", + }), + XAxis: charts.NewXAxisOption([]string{ + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun", + }), + SeriesList: []charts.Series{ + charts.NewSeriesFromValues([]float64{ + 120, + 132, + 101, + 134, + 90, + 230, + 210, + }), + }, + FillArea: true, + }, // 柱状图 { Title: charts.TitleOption{ @@ -1935,5 +1965,6 @@ func echartsHandler(w http.ResponseWriter, req *http.Request) { func main() { http.HandleFunc("/", indexHandler) http.HandleFunc("/echarts", echartsHandler) + fmt.Println("http://127.0.0.1:3012/") http.ListenAndServe(":3012", nil) } diff --git a/line_chart.go b/line_chart.go index 3942d70..0b44cdf 100644 --- a/line_chart.go +++ b/line_chart.go @@ -64,6 +64,8 @@ type LineChartOption struct { SymbolShow *bool // The stroke width of line StrokeWidth float64 + // Fill the area of line + FillArea bool // background is filled backgroundIsFilled bool } @@ -109,7 +111,6 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) ( StrokeWidth: strokeWidth, } - seriesPainter.SetDrawingStyle(drawingStyle) yRange := result.axisRanges[series.AxisIndex] points := make([]Point, 0) for i, item := range series.Data { @@ -120,6 +121,25 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) ( } points = append(points, p) } + // 如果需要填充区域 + if opt.FillArea { + areaPoints := make([]Point, len(points)) + copy(areaPoints, points) + bottomY := yRange.getRestHeight(yRange.min) + areaPoints = append(areaPoints, Point{ + X: areaPoints[len(areaPoints)-1].X, + Y: bottomY, + }, Point{ + X: areaPoints[0].X, + Y: bottomY, + }, areaPoints[0]) + seriesPainter.SetDrawingStyle(Style{ + FillColor: seriesColor.WithAlpha(200), + }) + seriesPainter.FillArea(areaPoints) + } + seriesPainter.SetDrawingStyle(drawingStyle) + // 画线 seriesPainter.LineStroke(points) From 128d5b277410e3c8c5a016dfcfade05f4bbd5cfb Mon Sep 17 00:00:00 2001 From: vicanso Date: Sun, 28 Aug 2022 09:43:18 +0800 Subject: [PATCH 061/116] refactor: adjust max value of axis, #19 --- mark_line_test.go | 2 +- range.go | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/mark_line_test.go b/mark_line_test.go index 84152ce..ef29e6f 100644 --- a/mark_line_test.go +++ b/mark_line_test.go @@ -67,7 +67,7 @@ func TestMarkLine(t *testing.T) { } return p.Bytes() }, - result: "\\n321", + result: "\\n321", }, } for _, tt := range tests { diff --git a/range.go b/range.go index 579a77f..ebd0b2d 100644 --- a/range.go +++ b/range.go @@ -60,7 +60,10 @@ func NewRange(opt AxisRangeOption) axisRange { r := math.Abs(max - min) // 最小单位计算 - unit := 2 + unit := 1 + if r > 5 { + unit = 2 + } if r > 10 { unit = 4 } @@ -85,6 +88,10 @@ func NewRange(opt AxisRangeOption) axisRange { } } max = min + float64(unit*divideCount) + expectMax := opt.Max * 2 + if max > expectMax { + max = float64(ceilFloatToInt(expectMax)) + } return axisRange{ divideCount: divideCount, min: min, From 4a1ff8055656382652b49cafc7ee22132884ebcc Mon Sep 17 00:00:00 2001 From: vicanso Date: Thu, 1 Sep 2022 20:20:51 +0800 Subject: [PATCH 062/116] fix: fix min and max option of y axis --- .github/workflows/test.yml | 1 + charts.go | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 22e77a8..61449a3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,6 +14,7 @@ jobs: strategy: matrix: go: + - '1.19' - '1.18' - '1.17' - '1.16' diff --git a/charts.go b/charts.go index 849f0c7..6d5dc56 100644 --- a/charts.go +++ b/charts.go @@ -174,12 +174,6 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e yAxisOption = opt.YAxisOptions[index] } max, min := opt.SeriesList.GetMaxMin(index) - if yAxisOption.Min != nil { - min = *yAxisOption.Min - } - if yAxisOption.Max != nil { - max = *yAxisOption.Max - } r := NewRange(AxisRangeOption{ Min: min, Max: max, @@ -188,6 +182,12 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e // 分隔数量 DivideCount: defaultAxisDivideCount, }) + if yAxisOption.Min != nil && *yAxisOption.Min <= min { + r.min = *yAxisOption.Min + } + if yAxisOption.Max != nil && *yAxisOption.Max >= max { + r.max = *yAxisOption.Max + } result.axisRanges[index] = r if yAxisOption.Theme == nil { From bb9af986be59b6c3be100e81ae54a4b713bf0a6c Mon Sep 17 00:00:00 2001 From: vicanso Date: Fri, 2 Sep 2022 20:42:10 +0800 Subject: [PATCH 063/116] chore: update go modules --- go.mod | 4 ++-- go.sum | 11 +++++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 66145c7..de0bb9c 100644 --- a/go.mod +++ b/go.mod @@ -5,13 +5,13 @@ go 1.17 require ( github.com/dustin/go-humanize v1.0.0 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 - github.com/stretchr/testify v1.7.2 + github.com/stretchr/testify v1.8.0 github.com/wcharczuk/go-chart/v2 v2.1.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/image v0.0.0-20220617043117-41969df76e82 // indirect + golang.org/x/image v0.0.0-20220902085622-e7cb96979f69 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 5f953b0..e0b1547 100644 --- a/go.sum +++ b/go.sum @@ -8,17 +8,20 @@ github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGw github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= -github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/wcharczuk/go-chart/v2 v2.1.0 h1:tY2slqVQ6bN+yHSnDYwZebLQFkphK4WNrVwnt7CJZ2I= github.com/wcharczuk/go-chart/v2 v2.1.0/go.mod h1:yx7MvAVNcP/kN9lKXM/NTce4au4DFN99j6i1OwDclNA= golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20220617043117-41969df76e82 h1:KpZB5pUSBvrHltNEdK/tw0xlPeD13M6M6aGP32gKqiw= -golang.org/x/image v0.0.0-20220617043117-41969df76e82/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY= +golang.org/x/image v0.0.0-20220902085622-e7cb96979f69 h1:Lj6HJGCSn5AjxRAH2+r35Mir4icalbqku+CLUtjnvXY= +golang.org/x/image v0.0.0-20220902085622-e7cb96979f69/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 50605907c761ba72f14f9f666775b394202195c8 Mon Sep 17 00:00:00 2001 From: vicanso Date: Thu, 15 Sep 2022 20:09:00 +0800 Subject: [PATCH 064/116] feat: support null value for line chart --- charts.go | 13 +++++++++++++ examples/line_chart/main.go | 3 ++- line_chart.go | 5 +++++ painter.go | 9 ++++++++- series.go | 4 ++++ 5 files changed, 32 insertions(+), 2 deletions(-) diff --git a/charts.go b/charts.go index 6d5dc56..f7e52e4 100644 --- a/charts.go +++ b/charts.go @@ -24,6 +24,7 @@ package charts import ( "errors" + "math" "sort" "github.com/wcharczuk/go-chart/v2" @@ -51,6 +52,18 @@ func SetDefaultHeight(height int) { } } +var nullValue = math.MaxFloat64 + +// SetNullValue sets the null value, default is MaxFloat64 +func SetNullValue(v float64) { + nullValue = v +} + +// GetNullValue gets the null value +func GetNullValue() float64 { + return nullValue +} + type Renderer interface { Render() (Box, error) } diff --git a/examples/line_chart/main.go b/examples/line_chart/main.go index 36eabee..97d5859 100644 --- a/examples/line_chart/main.go +++ b/examples/line_chart/main.go @@ -29,7 +29,8 @@ func main() { 120, 132, 101, - 134, + // 134, + charts.GetNullValue(), 90, 230, 210, diff --git a/line_chart.go b/line_chart.go index 0b44cdf..839aa6f 100644 --- a/line_chart.go +++ b/line_chart.go @@ -23,6 +23,8 @@ package charts import ( + "math" + "github.com/golang/freetype/truetype" "github.com/wcharczuk/go-chart/v2/drawing" ) @@ -115,6 +117,9 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) ( points := make([]Point, 0) for i, item := range series.Data { h := yRange.getRestHeight(item.Value) + if item.Value == nullValue { + h = math.MaxInt + } p := Point{ X: xValues[i], Y: h, diff --git a/painter.go b/painter.go index 1a954e2..f172cb3 100644 --- a/painter.go +++ b/painter.go @@ -438,11 +438,18 @@ func (p *Painter) MeasureTextMaxWidthHeight(textList []string) (int, int) { } func (p *Painter) LineStroke(points []Point) *Painter { + shouldMoveTo := false for index, point := range points { x := point.X y := point.Y - if index == 0 { + if y == math.MaxInt { + p.Stroke() + shouldMoveTo = true + continue + } + if shouldMoveTo || index == 0 { p.MoveTo(x, y) + shouldMoveTo = false } else { p.LineTo(x, y) } diff --git a/series.go b/series.go index ea71869..7bd6834 100644 --- a/series.go +++ b/series.go @@ -165,6 +165,10 @@ func (sl SeriesList) GetMaxMin(axisIndex int) (float64, float64) { continue } for _, item := range series.Data { + // 如果为空值,忽略 + if item.Value == nullValue { + continue + } if item.Value > max { max = item.Value } From 825e65d93078aee8ae99916ff916daf7a425a56d Mon Sep 17 00:00:00 2001 From: vicanso Date: Thu, 15 Sep 2022 20:15:05 +0800 Subject: [PATCH 065/116] refactor: use MaxInt32 instead of MaxInt --- line_chart.go | 2 +- painter.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/line_chart.go b/line_chart.go index 839aa6f..cdec280 100644 --- a/line_chart.go +++ b/line_chart.go @@ -118,7 +118,7 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) ( for i, item := range series.Data { h := yRange.getRestHeight(item.Value) if item.Value == nullValue { - h = math.MaxInt + h = int(math.MaxInt32) } p := Point{ X: xValues[i], diff --git a/painter.go b/painter.go index f172cb3..b7122b7 100644 --- a/painter.go +++ b/painter.go @@ -442,7 +442,7 @@ func (p *Painter) LineStroke(points []Point) *Painter { for index, point := range points { x := point.X y := point.Y - if y == math.MaxInt { + if y == int(math.MaxInt32) { p.Stroke() shouldMoveTo = true continue From de49ef8c68ea47881d103d6f650c17ac5a1ea14a Mon Sep 17 00:00:00 2001 From: vicanso Date: Thu, 22 Sep 2022 20:10:45 +0800 Subject: [PATCH 066/116] feat: support label for line chart, #23 --- bar_chart.go | 17 +++------------ line_chart.go | 30 ++++++++++++++++++++++++++ series_label.go | 56 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 14 deletions(-) create mode 100644 series_label.go diff --git a/bar_chart.go b/bar_chart.go index 26f8da5..797f710 100644 --- a/bar_chart.go +++ b/bar_chart.go @@ -62,13 +62,6 @@ type BarChartOption struct { Legend LegendOption } -type barChartLabelRenderOption struct { - Text string - Style Style - X int - Y int -} - func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) { p := b.p opt := b.opt @@ -100,11 +93,12 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B markPointPainter := NewMarkPointPainter(seriesPainter) markLinePainter := NewMarkLinePainter(seriesPainter) + labelPainter := NewSeriesLabelPainter(seriesPainter) rendererList := []Renderer{ + labelPainter, markPointPainter, markLinePainter, } - labelRenderOptions := make([]barChartLabelRenderOption, 0) for index := range seriesList { series := seriesList[index] yRange := result.axisRanges[series.AxisIndex] @@ -168,8 +162,7 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B } textBox := seriesPainter.MeasureText(text) - - labelRenderOptions = append(labelRenderOptions, barChartLabelRenderOption{ + labelPainter.Add(LabelValue{ Text: text, Style: labelStyle, X: x + (barWidth-textBox.Width())>>1, @@ -192,10 +185,6 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B Range: yRange, }) } - for _, labelOpt := range labelRenderOptions { - seriesPainter.OverrideTextStyle(labelOpt.Style) - seriesPainter.Text(labelOpt.Text, labelOpt.X, labelOpt.Y) - } // 最大、最小的mark point err := doRender(rendererList...) if err != nil { diff --git a/line_chart.go b/line_chart.go index cdec280..bf39ae2 100644 --- a/line_chart.go +++ b/line_chart.go @@ -97,7 +97,9 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) ( } markPointPainter := NewMarkPointPainter(seriesPainter) markLinePainter := NewMarkLinePainter(seriesPainter) + labelPainter := NewSeriesLabelPainter(seriesPainter) rendererList := []Renderer{ + labelPainter, markPointPainter, markLinePainter, } @@ -105,6 +107,8 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) ( if strokeWidth == 0 { strokeWidth = defaultStrokeWidth } + seriesNames := seriesList.Names() + theme := opt.Theme for index := range seriesList { series := seriesList[index] seriesColor := opt.Theme.GetSeriesColor(series.index) @@ -125,6 +129,32 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) ( Y: h, } points = append(points, p) + + // 如果label不需要展示,则返回 + if !series.Label.Show { + continue + } + distance := series.Label.Distance + if distance == 0 { + distance = 5 + } + text := NewValueLabelFormatter(seriesNames, series.Label.Formatter)(index, item.Value, -1) + labelStyle := Style{ + FontColor: theme.GetTextColor(), + FontSize: labelFontSize, + Font: opt.Font, + } + if !series.Label.Color.IsZero() { + labelStyle.FontColor = series.Label.Color + } + + textBox := seriesPainter.MeasureText(text) + labelPainter.Add(LabelValue{ + Text: text, + Style: labelStyle, + X: p.X - textBox.Width()>>1, + Y: p.Y - distance, + }) } // 如果需要填充区域 if opt.FillArea { diff --git a/series_label.go b/series_label.go new file mode 100644 index 0000000..c1850bb --- /dev/null +++ b/series_label.go @@ -0,0 +1,56 @@ +// 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 LabelValue struct { + Text string + Style Style + X int + Y int +} + +type SeriesLabelPainter struct { + p *Painter + values []LabelValue +} + +func NewSeriesLabelPainter(p *Painter) *SeriesLabelPainter { + return &SeriesLabelPainter{ + p: p, + values: make([]LabelValue, 0), + } +} + +func (o *SeriesLabelPainter) Add(value LabelValue) { + o.values = append(o.values, value) +} + +func (o *SeriesLabelPainter) Render() (Box, error) { + for _, item := range o.values { + o.p.OverrideTextStyle(item.Style) + o.p.Text(item.Text, item.X, item.Y) + } + return chart.BoxZero, nil +} From 1f5b9d513ee4387b2fc5a0f8a2b3ccb3836ad42f Mon Sep 17 00:00:00 2001 From: vicanso Date: Fri, 23 Sep 2022 20:50:42 +0800 Subject: [PATCH 067/116] refactor: adjust series label render --- bar_chart.go | 39 ++++++++++++--------------- bar_chart_test.go | 2 +- line_chart.go | 39 +++++++++++---------------- series_label.go | 69 ++++++++++++++++++++++++++++++++++++++++------- 4 files changed, 94 insertions(+), 55 deletions(-) diff --git a/bar_chart.go b/bar_chart.go index 797f710..8826ffb 100644 --- a/bar_chart.go +++ b/bar_chart.go @@ -93,9 +93,7 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B markPointPainter := NewMarkPointPainter(seriesPainter) markLinePainter := NewMarkLinePainter(seriesPainter) - labelPainter := NewSeriesLabelPainter(seriesPainter) rendererList := []Renderer{ - labelPainter, markPointPainter, markLinePainter, } @@ -106,6 +104,18 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B divideValues := xRange.AutoDivide() points := make([]Point, len(series.Data)) + var labelPainter *SeriesLabelPainter + if series.Label.Show { + labelPainter = NewSeriesLabelPainter(SeriesLabelPainterParams{ + P: seriesPainter, + SeriesNames: seriesNames, + Label: series.Label, + Theme: opt.Theme, + Font: opt.Font, + }) + rendererList = append(rendererList, labelPainter) + } + for j, item := range series.Data { if j >= xRange.divideCount { continue @@ -144,29 +154,14 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B Y: top, } // 如果label不需要展示,则返回 - if !series.Label.Show { + if labelPainter == nil { continue } - distance := series.Label.Distance - if distance == 0 { - distance = 5 - } - text := NewValueLabelFormatter(seriesNames, series.Label.Formatter)(index, item.Value, -1) - labelStyle := Style{ - FontColor: theme.GetTextColor(), - FontSize: labelFontSize, - Font: opt.Font, - } - if !series.Label.Color.IsZero() { - labelStyle.FontColor = series.Label.Color - } - - textBox := seriesPainter.MeasureText(text) labelPainter.Add(LabelValue{ - Text: text, - Style: labelStyle, - X: x + (barWidth-textBox.Width())>>1, - Y: barMaxHeight - h - distance, + Index: index, + Value: item.Value, + X: x + barWidth>>1, + Y: barMaxHeight - h, }) } diff --git a/bar_chart_test.go b/bar_chart_test.go index bee0583..e1522d6 100644 --- a/bar_chart_test.go +++ b/bar_chart_test.go @@ -102,7 +102,7 @@ func TestBarChart(t *testing.T) { } return p.Bytes() }, - result: "\\n24020016012080400FebMayAugNov24.9723.225.676.7135.6162.232.6206.43.32.65.9926.428.770.7175.6182.248.718.862.3", + result: "\\n24020016012080400FebMayAugNov24.9723.225.676.7135.6162.232.6206.43.32.65.9926.428.770.7175.6182.248.718.862.3", }, } diff --git a/line_chart.go b/line_chart.go index bf39ae2..26f94a4 100644 --- a/line_chart.go +++ b/line_chart.go @@ -97,9 +97,7 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) ( } markPointPainter := NewMarkPointPainter(seriesPainter) markLinePainter := NewMarkLinePainter(seriesPainter) - labelPainter := NewSeriesLabelPainter(seriesPainter) rendererList := []Renderer{ - labelPainter, markPointPainter, markLinePainter, } @@ -108,7 +106,6 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) ( strokeWidth = defaultStrokeWidth } seriesNames := seriesList.Names() - theme := opt.Theme for index := range seriesList { series := seriesList[index] seriesColor := opt.Theme.GetSeriesColor(series.index) @@ -119,6 +116,17 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) ( yRange := result.axisRanges[series.AxisIndex] points := make([]Point, 0) + var labelPainter *SeriesLabelPainter + if series.Label.Show { + labelPainter = NewSeriesLabelPainter(SeriesLabelPainterParams{ + P: seriesPainter, + SeriesNames: seriesNames, + Label: series.Label, + Theme: opt.Theme, + Font: opt.Font, + }) + rendererList = append(rendererList, labelPainter) + } for i, item := range series.Data { h := yRange.getRestHeight(item.Value) if item.Value == nullValue { @@ -131,29 +139,14 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) ( points = append(points, p) // 如果label不需要展示,则返回 - if !series.Label.Show { + if labelPainter == nil { continue } - distance := series.Label.Distance - if distance == 0 { - distance = 5 - } - text := NewValueLabelFormatter(seriesNames, series.Label.Formatter)(index, item.Value, -1) - labelStyle := Style{ - FontColor: theme.GetTextColor(), - FontSize: labelFontSize, - Font: opt.Font, - } - if !series.Label.Color.IsZero() { - labelStyle.FontColor = series.Label.Color - } - - textBox := seriesPainter.MeasureText(text) labelPainter.Add(LabelValue{ - Text: text, - Style: labelStyle, - X: p.X - textBox.Width()>>1, - Y: p.Y - distance, + Index: index, + Value: item.Value, + X: p.X, + Y: p.Y, }) } // 如果需要填充区域 diff --git a/series_label.go b/series_label.go index c1850bb..57bd1bf 100644 --- a/series_label.go +++ b/series_label.go @@ -22,29 +22,80 @@ package charts -import "github.com/wcharczuk/go-chart/v2" +import ( + "github.com/golang/freetype/truetype" + "github.com/wcharczuk/go-chart/v2" +) -type LabelValue struct { +type labelRenderValue struct { Text string Style Style X int Y int } -type SeriesLabelPainter struct { - p *Painter - values []LabelValue +type LabelValue struct { + Index int + Value float64 + X int + Y int } -func NewSeriesLabelPainter(p *Painter) *SeriesLabelPainter { +type SeriesLabelPainter struct { + p *Painter + seriesNames []string + label *SeriesLabel + theme ColorPalette + font *truetype.Font + values []labelRenderValue +} + +type SeriesLabelPainterParams struct { + P *Painter + SeriesNames []string + Label SeriesLabel + Theme ColorPalette + Font *truetype.Font +} + +func NewSeriesLabelPainter(params SeriesLabelPainterParams) *SeriesLabelPainter { return &SeriesLabelPainter{ - p: p, - values: make([]LabelValue, 0), + p: params.P, + seriesNames: params.SeriesNames, + label: ¶ms.Label, + theme: params.Theme, + font: params.Font, + values: make([]labelRenderValue, 0), } } func (o *SeriesLabelPainter) Add(value LabelValue) { - o.values = append(o.values, value) + label := o.label + distance := label.Distance + if distance == 0 { + distance = 5 + } + text := NewValueLabelFormatter(o.seriesNames, label.Formatter)(value.Index, value.Value, -1) + labelStyle := Style{ + FontColor: o.theme.GetTextColor(), + FontSize: labelFontSize, + Font: o.font, + } + if !label.Color.IsZero() { + labelStyle.FontColor = label.Color + } + o.p.OverrideDrawingStyle(labelStyle) + textBox := o.p.MeasureText(text) + renderValue := labelRenderValue{ + Text: text, + Style: labelStyle, + X: value.X - textBox.Width()>>1, + Y: value.Y - distance, + } + if textBox.Width()%2 != 0 { + renderValue.X++ + } + o.values = append(o.values, renderValue) } func (o *SeriesLabelPainter) Render() (Box, error) { From 0a80e7056f69f21d1561f4426877ab7fa376dd4f Mon Sep 17 00:00:00 2001 From: vicanso Date: Wed, 28 Sep 2022 20:29:22 +0800 Subject: [PATCH 068/116] feat: support setting bar width for bar chart, #24 --- bar_chart.go | 10 ++++++++-- chart_option.go | 2 ++ charts.go | 7 ++++--- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/bar_chart.go b/bar_chart.go index 8826ffb..2addd17 100644 --- a/bar_chart.go +++ b/bar_chart.go @@ -59,7 +59,8 @@ type BarChartOption struct { // The option of title Title TitleOption // The legend option - Legend LegendOption + Legend LegendOption + BarWidth int } func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) { @@ -86,7 +87,12 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B } seriesCount := len(seriesList) // 总的宽度-两个margin-(总数-1)的barMargin - barWidth := (width - 2*margin - barMargin*(seriesCount-1)) / len(seriesList) + barWidth := (width - 2*margin - barMargin*(seriesCount-1)) / seriesCount + if opt.BarWidth > 0 && opt.BarWidth < barWidth { + barWidth = opt.BarWidth + // 重新计算margin + margin = (width - len(seriesList)*barWidth - barMargin*(seriesCount-1)) / 2 + } barMaxHeight := seriesPainter.Height() theme := opt.Theme seriesNames := seriesList.Names() diff --git a/chart_option.go b/chart_option.go index 93b81ba..447ef52 100644 --- a/chart_option.go +++ b/chart_option.go @@ -66,6 +66,8 @@ type ChartOption struct { SymbolShow *bool // The stroke width of line chart LineStrokeWidth float64 + // The bar with of bar chart + BarWidth int // Fill the area of line chart FillArea bool // The child charts diff --git a/charts.go b/charts.go index f7e52e4..f8c94a3 100644 --- a/charts.go +++ b/charts.go @@ -354,9 +354,10 @@ func Render(opt ChartOption, opts ...OptionFunc) (*Painter, error) { if len(barSeriesList) != 0 { handler.Add(func() error { _, err := NewBarChart(p, BarChartOption{ - Theme: opt.theme, - Font: opt.font, - XAxis: opt.XAxis, + Theme: opt.theme, + Font: opt.font, + XAxis: opt.XAxis, + BarWidth: opt.BarWidth, }).render(renderResult, barSeriesList) return err }) From 6652ece0fed83a33ea6dbe7bd0d99c8b14e945bc Mon Sep 17 00:00:00 2001 From: vicanso Date: Thu, 29 Sep 2022 20:20:54 +0800 Subject: [PATCH 069/116] feat: support bar height for horizontal bar chart --- bar_chart.go | 2 +- chart_option.go | 2 ++ charts.go | 1 + horizontal_bar_chart.go | 9 +++++++-- 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/bar_chart.go b/bar_chart.go index 2addd17..d798c07 100644 --- a/bar_chart.go +++ b/bar_chart.go @@ -91,7 +91,7 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B if opt.BarWidth > 0 && opt.BarWidth < barWidth { barWidth = opt.BarWidth // 重新计算margin - margin = (width - len(seriesList)*barWidth - barMargin*(seriesCount-1)) / 2 + margin = (width - seriesCount*barWidth - barMargin*(seriesCount-1)) / 2 } barMaxHeight := seriesPainter.Height() theme := opt.Theme diff --git a/chart_option.go b/chart_option.go index 447ef52..f3bf2cb 100644 --- a/chart_option.go +++ b/chart_option.go @@ -68,6 +68,8 @@ type ChartOption struct { LineStrokeWidth float64 // The bar with of bar chart BarWidth int + // The bar height of horizontal bar chart + BarHeight int // Fill the area of line chart FillArea bool // The child charts diff --git a/charts.go b/charts.go index f8c94a3..c7923f1 100644 --- a/charts.go +++ b/charts.go @@ -369,6 +369,7 @@ func Render(opt ChartOption, opts ...OptionFunc) (*Painter, error) { _, err := NewHorizontalBarChart(p, HorizontalBarChartOption{ Theme: opt.theme, Font: opt.font, + BarHeight: opt.BarHeight, YAxisOptions: opt.YAxisOptions, }).render(renderResult, horizontalBarSeriesList) return err diff --git a/horizontal_bar_chart.go b/horizontal_bar_chart.go index 30a9b7d..8ffac44 100644 --- a/horizontal_bar_chart.go +++ b/horizontal_bar_chart.go @@ -48,7 +48,8 @@ type HorizontalBarChartOption struct { // The option of title Title TitleOption // The legend option - Legend LegendOption + Legend LegendOption + BarHeight int } // NewHorizontalBarChart returns a horizontal bar chart renderer @@ -82,7 +83,11 @@ func (h *horizontalBarChart) render(result *defaultRenderResult, seriesList Seri } seriesCount := len(seriesList) // 总的高度-两个margin-(总数-1)的barMargin - barHeight := (height - 2*margin - barMargin*(seriesCount-1)) / len(seriesList) + barHeight := (height - 2*margin - barMargin*(seriesCount-1)) / seriesCount + if opt.BarHeight > 0 && opt.BarHeight < barHeight { + barHeight = opt.BarHeight + margin = (height - seriesCount*barHeight - barMargin*(seriesCount-1)) / 2 + } theme := opt.Theme From 0a1061a8db90cf3829279afae2c72ca4968b7c8d Mon Sep 17 00:00:00 2001 From: vicanso Date: Tue, 11 Oct 2022 20:17:22 +0800 Subject: [PATCH 070/116] docs: update document --- README_zh.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README_zh.md b/README_zh.md index 87c42fa..c31cf77 100644 --- a/README_zh.md +++ b/README_zh.md @@ -569,7 +569,7 @@ BenchmarkMultiChartSVGRender-8 367 3356325 ns/op 默认使用的字符为`roboto`为英文字体库,因此如果需要显示中文字符需要增加中文字体库,`InstallFont`函数可添加对应的字体库,成功添加之后则指定`title.textStyle.fontFamily`即可。 在浏览器中使用`svg`时,如果指定的`fontFamily`不支持中文字符,展示的中文并不会乱码,但是会导致在计算字符宽度等错误。 -字体文件可以在[中文字库noto-cjk](https://github.com/googlefonts/noto-cjk)下载,注意下载时选择字体格式为 `ttf` 格式,如果选用 `otf` 格式可能会加载失败。 +字体文件可以在[中文字库noto-cjk](https://github.com/googlefonts/noto-cjk)下载,注意下载时选择字体格式为 `ttf` 格式,如果选用 `otf` 格式可能会加载失败,字体尽量选择Bold类型,否则生成的图片会有点模糊。 示例见 [examples/chinese/main.go](examples/chinese/main.go) From 74a47a9858bd82972d25f39ae64bae7783721353 Mon Sep 17 00:00:00 2001 From: vicanso Date: Thu, 20 Oct 2022 20:27:42 +0800 Subject: [PATCH 071/116] refactor: enhance value format, #28 --- util.go | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/util.go b/util.go index a33c6d2..f8a451e 100644 --- a/util.go +++ b/util.go @@ -160,15 +160,25 @@ func NewFloatPoint(f float64) *float64 { v := f return &v } + +const K_VALUE = float64(1000) +const M_VALUE = K_VALUE * K_VALUE +const G_VALUE = M_VALUE * K_VALUE +const T_VALUE = G_VALUE * K_VALUE + func commafWithDigits(value float64) string { decimals := 2 - m := float64(1000 * 1000) - if value >= m { - return humanize.CommafWithDigits(value/m, decimals) + "M" + if value >= T_VALUE { + return humanize.CommafWithDigits(value/T_VALUE, decimals) + "T" } - k := float64(1000) - if value >= k { - return humanize.CommafWithDigits(value/k, decimals) + "k" + if value >= G_VALUE { + return humanize.CommafWithDigits(value/G_VALUE, decimals) + "G" + } + if value >= M_VALUE { + return humanize.CommafWithDigits(value/M_VALUE, decimals) + "M" + } + if value >= K_VALUE { + return humanize.CommafWithDigits(value/K_VALUE, decimals) + "k" } return humanize.CommafWithDigits(value, decimals) } From a88e607bfc83b27502a54879ae749050683c9123 Mon Sep 17 00:00:00 2001 From: vicanso Date: Fri, 21 Oct 2022 20:37:09 +0800 Subject: [PATCH 072/116] refactor: support custom value formatter --- bar_chart.go | 1 + chart_option.go | 2 ++ charts.go | 8 ++++++-- examples/line_chart/main.go | 4 ++++ horizontal_bar_chart.go | 1 + mark_line_test.go | 1 + painter.go | 8 +++++++- range.go | 9 ++++++++- 8 files changed, 30 insertions(+), 4 deletions(-) diff --git a/bar_chart.go b/bar_chart.go index d798c07..19c1664 100644 --- a/bar_chart.go +++ b/bar_chart.go @@ -69,6 +69,7 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B seriesPainter := result.seriesPainter xRange := NewRange(AxisRangeOption{ + Painter: b.p, DivideCount: len(opt.XAxis.Data), Size: seriesPainter.Width(), }) diff --git a/chart_option.go b/chart_option.go index f3bf2cb..d4605a1 100644 --- a/chart_option.go +++ b/chart_option.go @@ -74,6 +74,8 @@ type ChartOption struct { FillArea bool // The child charts Children []ChartOption + // The value formatter + ValueFormatter ValueFormatter } // OptionFunc option function diff --git a/charts.go b/charts.go index c7923f1..b66437c 100644 --- a/charts.go +++ b/charts.go @@ -188,8 +188,9 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e } max, min := opt.SeriesList.GetMaxMin(index) r := NewRange(AxisRangeOption{ - Min: min, - Max: max, + Painter: p, + Min: min, + Max: max, // 高度需要减去x轴的高度 Size: rangeHeight, // 分隔数量 @@ -287,6 +288,9 @@ func Render(opt ChartOption, opts ...OptionFunc) (*Painter, error) { opt.Parent = p } p := opt.Parent + if opt.ValueFormatter != nil { + p.valueFormatter = opt.ValueFormatter + } if !opt.Box.IsZero() { p = p.Child(PainterBoxOption(opt.Box)) } diff --git a/examples/line_chart/main.go b/examples/line_chart/main.go index 97d5859..c1478a6 100644 --- a/examples/line_chart/main.go +++ b/examples/line_chart/main.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "io/ioutil" "os" "path/filepath" @@ -98,6 +99,9 @@ func main() { } opt.SymbolShow = charts.FalseFlag() opt.LineStrokeWidth = 1 + opt.ValueFormatter = func(f float64) string { + return fmt.Sprintf("%.0f", f) + } }, ) diff --git a/horizontal_bar_chart.go b/horizontal_bar_chart.go index 8ffac44..58c6e19 100644 --- a/horizontal_bar_chart.go +++ b/horizontal_bar_chart.go @@ -93,6 +93,7 @@ func (h *horizontalBarChart) render(result *defaultRenderResult, seriesList Seri max, min := seriesList.GetMaxMin(0) xRange := NewRange(AxisRangeOption{ + Painter: p, Min: min, Max: max, DivideCount: defaultAxisDivideCount, diff --git a/mark_line_test.go b/mark_line_test.go index ef29e6f..00d19ef 100644 --- a/mark_line_test.go +++ b/mark_line_test.go @@ -55,6 +55,7 @@ func TestMarkLine(t *testing.T) { StrokeColor: drawing.ColorBlack, Series: series, Range: NewRange(AxisRangeOption{ + Painter: p, Min: 0, Max: 5, Size: p.Height(), diff --git a/painter.go b/painter.go index b7122b7..efd5045 100644 --- a/painter.go +++ b/painter.go @@ -31,6 +31,8 @@ import ( "github.com/wcharczuk/go-chart/v2" ) +type ValueFormatter func(float64) string + type Painter struct { render chart.Renderer box Box @@ -39,7 +41,8 @@ type Painter struct { style Style theme ColorPalette // 类型 - outputType string + outputType string + valueFormatter ValueFormatter } type PainterOptions struct { @@ -188,6 +191,9 @@ func (p *Painter) setOptions(opts ...PainterOption) { func (p *Painter) Child(opt ...PainterOption) *Painter { child := &Painter{ + // 格式化 + valueFormatter: p.valueFormatter, + // render render: p.render, box: p.box.Clone(), font: p.font, diff --git a/range.go b/range.go index ebd0b2d..51d3332 100644 --- a/range.go +++ b/range.go @@ -29,6 +29,7 @@ import ( const defaultAxisDivideCount = 6 type axisRange struct { + p *Painter divideCount int min float64 max float64 @@ -37,6 +38,7 @@ type axisRange struct { } type AxisRangeOption struct { + Painter *Painter // The min value of axis Min float64 // The max value of axis @@ -93,6 +95,7 @@ func NewRange(opt AxisRangeOption) axisRange { max = float64(ceilFloatToInt(expectMax)) } return axisRange{ + p: opt.Painter, divideCount: divideCount, min: min, max: max, @@ -105,9 +108,13 @@ func NewRange(opt AxisRangeOption) axisRange { func (r axisRange) Values() []string { offset := (r.max - r.min) / float64(r.divideCount) values := make([]string, 0) + formatter := commafWithDigits + if r.p != nil && r.p.valueFormatter != nil { + formatter = r.p.valueFormatter + } for i := 0; i <= r.divideCount; i++ { v := r.min + float64(i)*offset - value := commafWithDigits(v) + value := formatter(v) values = append(values, value) } return values From bdcc871ab194dcaeeae1d934cc088bcffdff5fd2 Mon Sep 17 00:00:00 2001 From: vicanso Date: Thu, 3 Nov 2022 21:31:53 +0800 Subject: [PATCH 073/116] fix: fix series render of horizontal bar, #31 --- axis.go | 15 ++++++++++----- charts.go | 10 +++++++++- examples/horizontal_bar_chart/main.go | 3 +++ yaxis.go | 3 +++ 4 files changed, 25 insertions(+), 6 deletions(-) diff --git a/axis.go b/axis.go index 3c0484c..578813c 100644 --- a/axis.go +++ b/axis.go @@ -75,6 +75,7 @@ type AxisOption struct { SplitLineShow bool // The color of split line SplitLineColor Color + Unit int } func (a *axisPainter) Render() (Box, error) { @@ -159,11 +160,15 @@ func (a *axisPainter) Render() (Box, error) { // 根据文本宽度计算较为符合的展示项 fitTextCount := ceilFloatToInt(float64(top.Width()) / textFillWidth) - unit := ceilFloatToInt(float64(dataCount) / float64(fitTextCount)) - unit = chart.MaxInt(unit, opt.SplitNumber) - // 偶数 - if unit%2 == 0 && dataCount%(unit+1) == 0 { - unit++ + unit := opt.Unit + if unit <= 0 { + + unit = ceilFloatToInt(float64(dataCount) / float64(fitTextCount)) + unit = chart.MaxInt(unit, opt.SplitNumber) + // 偶数 + if unit%2 == 0 && dataCount%(unit+1) == 0 { + unit++ + } } width := 0 diff --git a/charts.go b/charts.go index b66437c..d6745d3 100644 --- a/charts.go +++ b/charts.go @@ -186,6 +186,10 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e if len(opt.YAxisOptions) > index { yAxisOption = opt.YAxisOptions[index] } + divideCount := yAxisOption.DivideCount + if divideCount <= 0 { + divideCount = defaultAxisDivideCount + } max, min := opt.SeriesList.GetMaxMin(index) r := NewRange(AxisRangeOption{ Painter: p, @@ -194,7 +198,7 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e // 高度需要减去x轴的高度 Size: rangeHeight, // 分隔数量 - DivideCount: defaultAxisDivideCount, + DivideCount: divideCount, }) if yAxisOption.Min != nil && *yAxisOption.Min <= min { r.min = *yAxisOption.Min @@ -346,6 +350,10 @@ func Render(opt ChartOption, opts ...OptionFunc) (*Painter, error) { }, } } + if len(horizontalBarSeriesList) != 0 { + renderOpt.YAxisOptions[0].DivideCount = len(renderOpt.YAxisOptions[0].Data) + renderOpt.YAxisOptions[0].Unit = 1 + } renderResult, err := defaultRender(p, renderOpt) if err != nil { diff --git a/examples/horizontal_bar_chart/main.go b/examples/horizontal_bar_chart/main.go index 8b996b6..a0f5bda 100644 --- a/examples/horizontal_bar_chart/main.go +++ b/examples/horizontal_bar_chart/main.go @@ -26,6 +26,7 @@ func writeFile(buf []byte) error { func main() { values := [][]float64{ { + 8203, 18203, 23489, 29034, @@ -34,6 +35,7 @@ func main() { 630230, }, { + 9325, 19325, 23438, 31000, @@ -56,6 +58,7 @@ func main() { "2012", }), charts.YAxisDataOptionFunc([]string{ + "UN", "Brazil", "Indonesia", "USA", diff --git a/yaxis.go b/yaxis.go index eb9034c..bece2cc 100644 --- a/yaxis.go +++ b/yaxis.go @@ -47,6 +47,8 @@ type YAxisOption struct { Color Color // The flag for show axis, set this to *false will hide axis Show *bool + DivideCount int + Unit int isCategoryAxis bool } @@ -87,6 +89,7 @@ func (opt *YAxisOption) ToAxisOption(p *Painter) AxisOption { SplitLineShow: true, SplitLineColor: theme.GetAxisSplitLineColor(), Show: opt.Show, + Unit: opt.Unit, } if !opt.Color.IsZero() { axisOpt.FontColor = opt.Color From 6f6d6c344730f48b5297bb9695a16f9ecc7874f1 Mon Sep 17 00:00:00 2001 From: vicanso Date: Mon, 7 Nov 2022 20:34:28 +0800 Subject: [PATCH 074/116] fix: fix label render of pie chart, #34 --- pie_chart.go | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/pie_chart.go b/pie_chart.go index 0075ffc..b4714ac 100644 --- a/pie_chart.go +++ b/pie_chart.go @@ -101,8 +101,23 @@ func (p *pieChart) render(result *defaultRenderResult, seriesList SeriesList) (B theme := opt.Theme currentValue := float64(0) - prevEndX := 0 - prevEndY := 0 + prevPoints := make([]Point, 0) + + isOverride := func(x, y int) bool { + for _, p := range prevPoints { + if math.Abs(float64(p.Y-y)) > labelFontSize { + continue + } + // label可能较多内容,不好计算横向占用空间 + // 因此x的位置需要中间位置两侧,否则认为override + if (p.X <= cx && x <= cx) || + (p.X > cx && x > cx) { + return true + } + } + return false + } + for index, v := range values { seriesPainter.OverrideDrawingStyle(Style{ StrokeWidth: 1, @@ -134,13 +149,17 @@ func (p *pieChart) render(result *defaultRenderResult, seriesList SeriesList) (B endx := cx + int(labelRadius*math.Cos(angle)) endy := cy + int(labelRadius*math.Sin(angle)) // 计算是否有重叠,如果有则调整y坐标位置 - if index != 0 && - math.Abs(float64(endx-prevEndX)) < labelFontSize && - math.Abs(float64(endy-prevEndY)) < labelFontSize { + // 最多只尝试5次 + for i := 0; i < 5; i++ { + if !isOverride(endx, endy) { + break + } endy -= (labelFontSize << 1) } - prevEndX = endx - prevEndY = endy + prevPoints = append(prevPoints, Point{ + X: endx, + Y: endy, + }) seriesPainter.MoveTo(startx, starty) seriesPainter.LineTo(endx, endy) From 2ed86a81d018bcf9d0105bf217a9c424aa42bf5e Mon Sep 17 00:00:00 2001 From: vicanso Date: Sat, 12 Nov 2022 10:48:24 +0800 Subject: [PATCH 075/116] fix: fix setting font family for table render --- chart_option.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/chart_option.go b/chart_option.go index d4605a1..3c8ac4b 100644 --- a/chart_option.go +++ b/chart_option.go @@ -384,6 +384,9 @@ func TableOptionRender(opt TableChartOption) (*Painter, error) { if opt.Width <= 0 { opt.Width = defaultChartWidth } + if opt.FontFamily != "" { + opt.Font, _ = GetFont(opt.FontFamily) + } if opt.Font == nil { opt.Font, _ = chart.GetDefaultFont() } From de4250f60bfad7d22847b089bef62f2dce30091b Mon Sep 17 00:00:00 2001 From: vicanso Date: Sat, 12 Nov 2022 20:01:36 +0800 Subject: [PATCH 076/116] feat: support get and set default font --- chart_option.go | 5 ++--- font.go | 19 ++++++++++++++++++- mark_line.go | 3 +-- painter.go | 2 +- painter_test.go | 2 +- theme.go | 3 +-- 6 files changed, 24 insertions(+), 10 deletions(-) diff --git a/chart_option.go b/chart_option.go index 3c8ac4b..ee6851f 100644 --- a/chart_option.go +++ b/chart_option.go @@ -26,7 +26,6 @@ import ( "sort" "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2" ) type ChartOption struct { @@ -270,7 +269,7 @@ func (o *ChartOption) fillDefault() { o.font, _ = GetFont(o.FontFamily) if o.font == nil { - o.font, _ = chart.GetDefaultFont() + o.font, _ = GetDefaultFont() } else { // 如果指定了字体,则设置主题的字体 t.SetFont(o.font) @@ -388,7 +387,7 @@ func TableOptionRender(opt TableChartOption) (*Painter, error) { opt.Font, _ = GetFont(opt.FontFamily) } if opt.Font == nil { - opt.Font, _ = chart.GetDefaultFont() + opt.Font, _ = GetDefaultFont() } p, err := NewPainter(PainterOptions{ diff --git a/font.go b/font.go index c40b51e..dae5141 100644 --- a/font.go +++ b/font.go @@ -32,9 +32,13 @@ import ( var fonts = sync.Map{} var ErrFontNotExists = errors.New("font is not exists") +var defaultFontFamily = "defaultFontFamily" func init() { - _ = InstallFont("roboto", roboto.Roboto) + name := "roboto" + _ = InstallFont(name, roboto.Roboto) + font, _ := GetFont(name) + SetDefaultFont(font) } // InstallFont installs the font for charts @@ -47,6 +51,19 @@ func InstallFont(fontFamily string, data []byte) error { return nil } +// GetDefaultFont get default font +func GetDefaultFont() (*truetype.Font, error) { + return GetFont(defaultFontFamily) +} + +// SetDefaultFont set default font +func SetDefaultFont(font *truetype.Font) { + if font == nil { + return + } + fonts.Store(defaultFontFamily, font) +} + // GetFont get the font by font family func GetFont(fontFamily string) (*truetype.Font, error) { value, ok := fonts.Load(fontFamily) diff --git a/mark_line.go b/mark_line.go index af1062d..bc850bb 100644 --- a/mark_line.go +++ b/mark_line.go @@ -24,7 +24,6 @@ package charts import ( "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2" ) // NewMarkLine returns a series mark line @@ -75,7 +74,7 @@ func (m *markLinePainter) Render() (Box, error) { } font := opt.Font if font == nil { - font, _ = chart.GetDefaultFont() + font, _ = GetDefaultFont() } summary := s.Summary() for _, markLine := range s.MarkLine.Data { diff --git a/painter.go b/painter.go index efd5045..97ad205 100644 --- a/painter.go +++ b/painter.go @@ -149,7 +149,7 @@ func NewPainter(opts PainterOptions, opt ...PainterOption) (*Painter, error) { } font := opts.Font if font == nil { - f, err := chart.GetDefaultFont() + f, err := GetDefaultFont() if err != nil { return nil, err } diff --git a/painter_test.go b/painter_test.go index 96e41ef..2392d5b 100644 --- a/painter_test.go +++ b/painter_test.go @@ -351,7 +351,7 @@ func TestPainterTextFit(t *testing.T) { Type: ChartOutputSVG, }) assert.Nil(err) - f, _ := chart.GetDefaultFont() + f, _ := GetDefaultFont() style := Style{ FontSize: 12, FontColor: chart.ColorBlack, diff --git a/theme.go b/theme.go index 8068687..17706ad 100644 --- a/theme.go +++ b/theme.go @@ -24,7 +24,6 @@ package charts import ( "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2" "github.com/wcharczuk/go-chart/v2/drawing" ) @@ -311,7 +310,7 @@ func (t *themeColorPalette) GetFont() *truetype.Font { if t.font != nil { return t.font } - f, _ := chart.GetDefaultFont() + f, _ := GetDefaultFont() return f } From 7e1f003be85d09216e71a89337634bd38e4abed2 Mon Sep 17 00:00:00 2001 From: vicanso Date: Sat, 12 Nov 2022 20:18:02 +0800 Subject: [PATCH 077/116] refactor: update demo --- examples/chinese/main.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/chinese/main.go b/examples/chinese/main.go index 9068a08..d77216a 100644 --- a/examples/chinese/main.go +++ b/examples/chinese/main.go @@ -34,6 +34,8 @@ func main() { if err != nil { panic(err) } + font, _ := charts.GetFont("noto") + charts.SetDefaultFont(font) values := [][]float64{ { @@ -85,7 +87,6 @@ func main() { p, err := charts.LineRender( values, charts.TitleTextOptionFunc("测试"), - charts.FontFamilyOptionFunc("noto"), charts.XAxisDataOptionFunc([]string{ "星期一", "星期二", From a42d0727df41f5f788ae015c9472a5675bf27774 Mon Sep 17 00:00:00 2001 From: vicanso Date: Tue, 15 Nov 2022 20:09:29 +0800 Subject: [PATCH 078/116] feat: support text rotation --- painter.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/painter.go b/painter.go index 97ad205..6743b37 100644 --- a/painter.go +++ b/painter.go @@ -558,6 +558,12 @@ func (p *Painter) Text(body string, x, y int) *Painter { return p } +func (p *Painter) TextRotation(body string, x, y int, radians float64) { + p.render.SetTextRotation(radians) + p.render.Text(body, x, y) + p.render.ClearTextRotation() +} + func (p *Painter) TextFit(body string, x, y, width int, textAligns ...string) chart.Box { style := p.style textWarp := style.TextWrap From 55eca7b0b9331b660ea1f1c03fab3d803769e815 Mon Sep 17 00:00:00 2001 From: vicanso Date: Wed, 16 Nov 2022 20:46:19 +0800 Subject: [PATCH 079/116] feat: support detect color dark or light --- chart_option_test.go | 2 +- echarts_test.go | 2 +- mark_point.go | 12 +++++------- theme.go | 13 +++++++++++++ util.go | 7 +++++++ util_test.go | 32 ++++++++++++++++++++++++++++++++ 6 files changed, 59 insertions(+), 9 deletions(-) diff --git a/chart_option_test.go b/chart_option_test.go index 6f331b3..ff17750 100644 --- a/chart_option_test.go +++ b/chart_option_test.go @@ -277,7 +277,7 @@ func TestBarRender(t *testing.T) { assert.Nil(err) data, err := p.Bytes() assert.Nil(err) - assert.Equal("\\nRainfallEvaporation24020016012080400FebMayAugNov162.22182.22.341.6248.07", string(data)) + assert.Equal("\\nRainfallEvaporation24020016012080400FebMayAugNov162.22182.22.341.6248.07", string(data)) } func TestHorizontalBarRender(t *testing.T) { diff --git a/echarts_test.go b/echarts_test.go index 5c2dbad..2ce1715 100644 --- a/echarts_test.go +++ b/echarts_test.go @@ -578,5 +578,5 @@ func TestRenderEChartsToSVG(t *testing.T) { ] }`) assert.Nil(err) - assert.Equal("\\nRainfallEvaporationRainfall vs EvaporationFake Data24020016012080400FebMayAugNov162.22182.22.341.6248.07", string(data)) + assert.Equal("\\nRainfallEvaporationRainfall vs EvaporationFake Data24020016012080400FebMayAugNov162.22182.22.341.6248.07", string(data)) } diff --git a/mark_point.go b/mark_point.go index f6c93f3..fd8a88b 100644 --- a/mark_point.go +++ b/mark_point.go @@ -24,7 +24,6 @@ package charts import ( "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2/drawing" ) // NewMarkPoint returns a series mark point @@ -78,16 +77,15 @@ func (m *markPointPainter) Render() (Box, error) { symbolSize = 30 } textStyle := Style{ - FontColor: drawing.Color{ - R: 238, - G: 238, - B: 238, - A: 255, - }, FontSize: labelFontSize, StrokeWidth: 1, Font: opt.Font, } + if isLightColor(opt.FillColor) { + textStyle.FontColor = defaultLightFontColor + } else { + textStyle.FontColor = defaultDarkFontColor + } painter.OverrideDrawingStyle(Style{ FillColor: opt.FillColor, }).OverrideTextStyle(textStyle) diff --git a/theme.go b/theme.go index 17706ad..a6d624f 100644 --- a/theme.go +++ b/theme.go @@ -76,6 +76,19 @@ const defaultFontSize = 12.0 var defaultTheme ColorPalette +var defaultLightFontColor = drawing.Color{ + R: 70, + G: 70, + B: 70, + A: 255, +} +var defaultDarkFontColor = drawing.Color{ + R: 238, + G: 238, + B: 238, + A: 255, +} + func init() { echartSeriesColors := []Color{ parseColor("#5470c6"), diff --git a/util.go b/util.go index f8a451e..b333e6d 100644 --- a/util.go +++ b/util.go @@ -262,3 +262,10 @@ func getPolygonPoints(center Point, radius float64, sides int) []Point { } return points } + +func isLightColor(c Color) bool { + r := float64(c.R) * float64(c.R) * 0.299 + g := float64(c.G) * float64(c.G) * 0.587 + b := float64(c.B) * float64(c.B) * 0.114 + return math.Sqrt(r+g+b) > 127.5 +} diff --git a/util_test.go b/util_test.go index 7c2ab2f..62fd08d 100644 --- a/util_test.go +++ b/util_test.go @@ -189,3 +189,35 @@ func TestParseColor(t *testing.T) { A: 250, }, c) } + +func TestIsLightColor(t *testing.T) { + assert := assert.New(t) + + assert.True(isLightColor(drawing.Color{ + R: 255, + G: 255, + B: 255, + })) + assert.True(isLightColor(drawing.Color{ + R: 145, + G: 204, + B: 117, + })) + + assert.False(isLightColor(drawing.Color{ + R: 88, + G: 112, + B: 198, + })) + + assert.False(isLightColor(drawing.Color{ + R: 0, + G: 0, + B: 0, + })) + assert.False(isLightColor(drawing.Color{ + R: 16, + G: 12, + B: 42, + })) +} From 4fc250aefc0ec4e50099ca483b2a7a3f497b33a8 Mon Sep 17 00:00:00 2001 From: vicanso Date: Tue, 22 Nov 2022 22:41:56 +0800 Subject: [PATCH 080/116] feat: support rotate series label --- bar_chart.go | 19 +++++++++++++++++- examples/charts/main.go | 4 ++++ painter.go | 9 ++++++++- series.go | 2 ++ series_label.go | 44 +++++++++++++++++++++++++++++++---------- 5 files changed, 66 insertions(+), 12 deletions(-) diff --git a/bar_chart.go b/bar_chart.go index 19c1664..695b9fd 100644 --- a/bar_chart.go +++ b/bar_chart.go @@ -23,6 +23,8 @@ package charts import ( + "math" + "github.com/golang/freetype/truetype" "github.com/wcharczuk/go-chart/v2" ) @@ -164,11 +166,26 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B if labelPainter == nil { continue } + y := barMaxHeight - h + radians := float64(0) + var fontColor Color + if series.Label.Position == PositionBottom { + y = barMaxHeight + radians = -math.Pi / 2 + if isLightColor(fillColor) { + fontColor = defaultLightFontColor + } else { + fontColor = defaultDarkFontColor + } + } labelPainter.Add(LabelValue{ Index: index, Value: item.Value, X: x + barWidth>>1, - Y: barMaxHeight - h, + Y: y, + // 旋转 + Radians: radians, + FontColor: fontColor, }) } diff --git a/examples/charts/main.go b/examples/charts/main.go index c3bb486..76aa42c 100644 --- a/examples/charts/main.go +++ b/examples/charts/main.go @@ -355,6 +355,10 @@ func indexHandler(w http.ResponseWriter, req *http.Request) { Value: 180, }, }, + Label: charts.SeriesLabel{ + Show: true, + Position: charts.PositionBottom, + }, }, }, }, diff --git a/painter.go b/painter.go index 6743b37..a0f81ed 100644 --- a/painter.go +++ b/painter.go @@ -560,7 +560,14 @@ func (p *Painter) Text(body string, x, y int) *Painter { func (p *Painter) TextRotation(body string, x, y int, radians float64) { p.render.SetTextRotation(radians) - p.render.Text(body, x, y) + p.render.Text(body, x+p.box.Left, y+p.box.Top) + p.render.ClearTextRotation() +} + +func (p *Painter) SetTextRotation(radians float64) { + p.render.SetTextRotation(radians) +} +func (p *Painter) ClearTextRotation() { p.render.ClearTextRotation() } diff --git a/series.go b/series.go index 7bd6834..373c7dc 100644 --- a/series.go +++ b/series.go @@ -79,6 +79,8 @@ type SeriesLabel struct { Show bool // Distance to the host graphic element. Distance int + // The position of label + Position string } const ( diff --git a/series_label.go b/series_label.go index 57bd1bf..f2dd40f 100644 --- a/series_label.go +++ b/series_label.go @@ -32,6 +32,8 @@ type labelRenderValue struct { Style Style X int Y int + // 旋转 + Radians float64 } type LabelValue struct { @@ -39,6 +41,10 @@ type LabelValue struct { Value float64 X int Y int + // 旋转 + Radians float64 + // 字体颜色 + FontColor Color } type SeriesLabelPainter struct { @@ -81,19 +87,33 @@ func (o *SeriesLabelPainter) Add(value LabelValue) { FontSize: labelFontSize, Font: o.font, } + if !value.FontColor.IsZero() { + label.Color = value.FontColor + } if !label.Color.IsZero() { labelStyle.FontColor = label.Color } - o.p.OverrideDrawingStyle(labelStyle) - textBox := o.p.MeasureText(text) - renderValue := labelRenderValue{ - Text: text, - Style: labelStyle, - X: value.X - textBox.Width()>>1, - Y: value.Y - distance, + p := o.p + p.OverrideDrawingStyle(labelStyle) + rotated := value.Radians != 0 + if rotated { + p.SetTextRotation(value.Radians) } - if textBox.Width()%2 != 0 { - renderValue.X++ + textBox := p.MeasureText(text) + renderValue := labelRenderValue{ + Text: text, + Style: labelStyle, + X: value.X - textBox.Width()>>1, + Y: value.Y - distance, + Radians: value.Radians, + } + if rotated { + renderValue.X = value.X + textBox.Width()>>1 - 1 + p.ClearTextRotation() + } else { + if textBox.Width()%2 != 0 { + renderValue.X++ + } } o.values = append(o.values, renderValue) } @@ -101,7 +121,11 @@ func (o *SeriesLabelPainter) Add(value LabelValue) { func (o *SeriesLabelPainter) Render() (Box, error) { for _, item := range o.values { o.p.OverrideTextStyle(item.Style) - o.p.Text(item.Text, item.X, item.Y) + if item.Radians != 0 { + o.p.TextRotation(item.Text, item.X, item.Y, item.Radians) + } else { + o.p.Text(item.Text, item.X, item.Y) + } } return chart.BoxZero, nil } From 6db8e2c8dc5f5ead957474fddb4af20787b82b95 Mon Sep 17 00:00:00 2001 From: vicanso Date: Wed, 23 Nov 2022 23:01:52 +0800 Subject: [PATCH 081/116] feat: support series label for horizontal bar --- bar_chart.go | 1 + horizontal_bar_chart.go | 41 +++++++++++++++++++++++++++++++++++++++++ series.go | 2 ++ series_label.go | 16 ++++++++++++++-- 4 files changed, 58 insertions(+), 2 deletions(-) diff --git a/bar_chart.go b/bar_chart.go index 695b9fd..8219472 100644 --- a/bar_chart.go +++ b/bar_chart.go @@ -186,6 +186,7 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B // 旋转 Radians: radians, FontColor: fontColor, + Offset: series.Label.Offset, }) } diff --git a/horizontal_bar_chart.go b/horizontal_bar_chart.go index 58c6e19..5e433a6 100644 --- a/horizontal_bar_chart.go +++ b/horizontal_bar_chart.go @@ -99,11 +99,25 @@ func (h *horizontalBarChart) render(result *defaultRenderResult, seriesList Seri DivideCount: defaultAxisDivideCount, Size: seriesPainter.Width(), }) + seriesNames := seriesList.Names() + rendererList := []Renderer{} for index := range seriesList { series := seriesList[index] seriesColor := theme.GetSeriesColor(series.index) divideValues := yRange.AutoDivide() + + var labelPainter *SeriesLabelPainter + if series.Label.Show { + labelPainter = NewSeriesLabelPainter(SeriesLabelPainterParams{ + P: seriesPainter, + SeriesNames: seriesNames, + Label: series.Label, + Theme: opt.Theme, + Font: opt.Font, + }) + rendererList = append(rendererList, labelPainter) + } for j, item := range series.Data { if j >= yRange.divideCount { continue @@ -130,8 +144,35 @@ func (h *horizontalBarChart) render(result *defaultRenderResult, seriesList Seri Right: right, Bottom: y + barHeight, }) + // 如果label不需要展示,则返回 + if labelPainter == nil { + continue + } + x := right + var fontColor Color + if series.Label.Position == PositionLeft { + x = 0 + if isLightColor(fillColor) { + fontColor = defaultLightFontColor + } else { + fontColor = defaultDarkFontColor + } + } + labelPainter.Add(LabelValue{ + Orient: OrientHorizontal, + Index: index, + Value: item.Value, + X: x, + Y: y + barHeight>>1, + FontColor: fontColor, + Offset: series.Label.Offset, + }) } } + err := doRender(rendererList...) + if err != nil { + return BoxZero, err + } return p.box, nil } diff --git a/series.go b/series.go index 373c7dc..c36fa8b 100644 --- a/series.go +++ b/series.go @@ -81,6 +81,8 @@ type SeriesLabel struct { Distance int // The position of label Position string + // The offset of label's position + Offset Box } const ( diff --git a/series_label.go b/series_label.go index f2dd40f..f0fb2ec 100644 --- a/series_label.go +++ b/series_label.go @@ -45,6 +45,8 @@ type LabelValue struct { Radians float64 // 字体颜色 FontColor Color + Orient string + Offset Box } type SeriesLabelPainter struct { @@ -103,10 +105,18 @@ func (o *SeriesLabelPainter) Add(value LabelValue) { renderValue := labelRenderValue{ Text: text, Style: labelStyle, - X: value.X - textBox.Width()>>1, - Y: value.Y - distance, + X: value.X, + Y: value.Y, Radians: value.Radians, } + if value.Orient != OrientHorizontal { + renderValue.X -= textBox.Width() >> 1 + renderValue.Y -= distance + } else { + renderValue.X += distance + renderValue.Y += textBox.Height() >> 1 + renderValue.Y -= 2 + } if rotated { renderValue.X = value.X + textBox.Width()>>1 - 1 p.ClearTextRotation() @@ -115,6 +125,8 @@ func (o *SeriesLabelPainter) Add(value LabelValue) { renderValue.X++ } } + renderValue.X += value.Offset.Left + renderValue.Y += value.Offset.Top o.values = append(o.values, renderValue) } From 5f0aec60d3d3300316bd9d4d1ad58e357f7c4caf Mon Sep 17 00:00:00 2001 From: vicanso Date: Thu, 24 Nov 2022 20:12:19 +0800 Subject: [PATCH 082/116] refactor: adjust label value of horizontal bar --- horizontal_bar_chart.go | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/horizontal_bar_chart.go b/horizontal_bar_chart.go index 5e433a6..1340103 100644 --- a/horizontal_bar_chart.go +++ b/horizontal_bar_chart.go @@ -148,25 +148,23 @@ func (h *horizontalBarChart) render(result *defaultRenderResult, seriesList Seri if labelPainter == nil { continue } - x := right - var fontColor Color + labelValue := LabelValue{ + Orient: OrientHorizontal, + Index: index, + Value: item.Value, + X: right, + Y: y + barHeight>>1, + Offset: series.Label.Offset, + } if series.Label.Position == PositionLeft { - x = 0 + labelValue.X = 0 if isLightColor(fillColor) { - fontColor = defaultLightFontColor + labelValue.FontColor = defaultLightFontColor } else { - fontColor = defaultDarkFontColor + labelValue.FontColor = defaultDarkFontColor } } - labelPainter.Add(LabelValue{ - Orient: OrientHorizontal, - Index: index, - Value: item.Value, - X: x, - Y: y + barHeight>>1, - FontColor: fontColor, - Offset: series.Label.Offset, - }) + labelPainter.Add(labelValue) } } err := doRender(rendererList...) From df6180e59aea0a7d3d45be72a9b49bb4a09df0c9 Mon Sep 17 00:00:00 2001 From: vicanso Date: Mon, 28 Nov 2022 19:55:14 +0800 Subject: [PATCH 083/116] fix: fix zero max value of nan, #37 --- range.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/range.go b/range.go index 51d3332..ec64c2d 100644 --- a/range.go +++ b/range.go @@ -121,6 +121,9 @@ func (r axisRange) Values() []string { } func (r *axisRange) getHeight(value float64) int { + if r.max <= r.min { + return 0 + } v := (value - r.min) / (r.max - r.min) return int(v * float64(r.size)) } From f9a534ea02fe56f1c4ec79c73839664ba8cb51a6 Mon Sep 17 00:00:00 2001 From: vicanso Date: Wed, 7 Dec 2022 19:57:35 +0800 Subject: [PATCH 084/116] fix: fix the color of series label, #37 --- bar_chart.go | 12 +++++++----- horizontal_bar_chart.go | 23 +++++++++++++---------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/bar_chart.go b/bar_chart.go index 8219472..d8a307e 100644 --- a/bar_chart.go +++ b/bar_chart.go @@ -168,14 +168,16 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B } y := barMaxHeight - h radians := float64(0) - var fontColor Color + fontColor := series.Label.Color if series.Label.Position == PositionBottom { y = barMaxHeight radians = -math.Pi / 2 - if isLightColor(fillColor) { - fontColor = defaultLightFontColor - } else { - fontColor = defaultDarkFontColor + if fontColor.IsZero() { + if isLightColor(fillColor) { + fontColor = defaultLightFontColor + } else { + fontColor = defaultDarkFontColor + } } } labelPainter.Add(LabelValue{ diff --git a/horizontal_bar_chart.go b/horizontal_bar_chart.go index 1340103..95d9a3d 100644 --- a/horizontal_bar_chart.go +++ b/horizontal_bar_chart.go @@ -149,19 +149,22 @@ func (h *horizontalBarChart) render(result *defaultRenderResult, seriesList Seri continue } labelValue := LabelValue{ - Orient: OrientHorizontal, - Index: index, - Value: item.Value, - X: right, - Y: y + barHeight>>1, - Offset: series.Label.Offset, + Orient: OrientHorizontal, + Index: index, + Value: item.Value, + X: right, + Y: y + barHeight>>1, + Offset: series.Label.Offset, + FontColor: series.Label.Color, } if series.Label.Position == PositionLeft { labelValue.X = 0 - if isLightColor(fillColor) { - labelValue.FontColor = defaultLightFontColor - } else { - labelValue.FontColor = defaultDarkFontColor + if labelValue.FontColor.IsZero() { + if isLightColor(fillColor) { + labelValue.FontColor = defaultLightFontColor + } else { + labelValue.FontColor = defaultDarkFontColor + } } } labelPainter.Add(labelValue) From ef04ac14abcfe6380464fdc4b3d923448286e198 Mon Sep 17 00:00:00 2001 From: vicanso Date: Fri, 9 Dec 2022 20:08:02 +0800 Subject: [PATCH 085/116] feat: support font size for series label, #38 --- bar_chart.go | 1 + horizontal_bar_chart.go | 1 + line_chart.go | 2 ++ series.go | 2 ++ series_label.go | 9 +++++++-- 5 files changed, 13 insertions(+), 2 deletions(-) diff --git a/bar_chart.go b/bar_chart.go index d8a307e..efeb465 100644 --- a/bar_chart.go +++ b/bar_chart.go @@ -189,6 +189,7 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B Radians: radians, FontColor: fontColor, Offset: series.Label.Offset, + FontSize: series.Label.FontSize, }) } diff --git a/horizontal_bar_chart.go b/horizontal_bar_chart.go index 95d9a3d..2ab4c03 100644 --- a/horizontal_bar_chart.go +++ b/horizontal_bar_chart.go @@ -156,6 +156,7 @@ func (h *horizontalBarChart) render(result *defaultRenderResult, seriesList Seri Y: y + barHeight>>1, Offset: series.Label.Offset, FontColor: series.Label.Color, + FontSize: series.Label.FontSize, } if series.Label.Position == PositionLeft { labelValue.X = 0 diff --git a/line_chart.go b/line_chart.go index 26f94a4..9f350bd 100644 --- a/line_chart.go +++ b/line_chart.go @@ -147,6 +147,8 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) ( Value: item.Value, X: p.X, Y: p.Y, + // 字体大小 + FontSize: series.Label.FontSize, }) } // 如果需要填充区域 diff --git a/series.go b/series.go index c36fa8b..13c637e 100644 --- a/series.go +++ b/series.go @@ -83,6 +83,8 @@ type SeriesLabel struct { Position string // The offset of label's position Offset Box + // The font size of label + FontSize float64 } const ( diff --git a/series_label.go b/series_label.go index f0fb2ec..10fd148 100644 --- a/series_label.go +++ b/series_label.go @@ -45,8 +45,10 @@ type LabelValue struct { Radians float64 // 字体颜色 FontColor Color - Orient string - Offset Box + // 字体大小 + FontSize float64 + Orient string + Offset Box } type SeriesLabelPainter struct { @@ -89,6 +91,9 @@ func (o *SeriesLabelPainter) Add(value LabelValue) { FontSize: labelFontSize, Font: o.font, } + if value.FontSize != 0 { + labelStyle.FontSize = value.FontSize + } if !value.FontColor.IsZero() { label.Color = value.FontColor } From d5533447f565ed55b604a59d0578375c61d496cd Mon Sep 17 00:00:00 2001 From: vicanso Date: Sun, 11 Dec 2022 14:57:05 +0800 Subject: [PATCH 086/116] feat: support text rotation for series label, #38 --- axis.go | 26 ++++++++++++++++++++------ painter.go | 9 +++++++++ xaxis.go | 8 +++++++- 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/axis.go b/axis.go index 578813c..3f71451 100644 --- a/axis.go +++ b/axis.go @@ -75,7 +75,11 @@ type AxisOption struct { SplitLineShow bool // The color of split line SplitLineColor Color - Unit int + // The text rotation of label + TextRotation float64 + // The offset of label + LabelOffset Box + Unit int } func (a *axisPainter) Render() (Box, error) { @@ -153,7 +157,15 @@ func (a *axisPainter) Render() (Box, error) { } top.SetDrawingStyle(style).OverrideTextStyle(style) + isTextRotation := opt.TextRotation != 0 + + if isTextRotation { + top.SetTextRotation(opt.TextRotation) + } textMaxWidth, textMaxHeight := top.MeasureTextMaxWidthHeight(data) + if isTextRotation { + top.ClearTextRotation() + } // 增加30px来计算文本展示区域 textFillWidth := float64(textMaxWidth + 20) @@ -261,11 +273,13 @@ func (a *axisPainter) Render() (Box, error) { Top: labelPaddingTop, Right: labelPaddingRight, })).MultiText(MultiTextOption{ - Align: textAlign, - TextList: data, - Orient: orient, - Unit: unit, - Position: labelPosition, + Align: textAlign, + TextList: data, + Orient: orient, + Unit: unit, + Position: labelPosition, + TextRotation: opt.TextRotation, + Offset: opt.LabelOffset, }) // 显示辅助线 if opt.SplitLineShow { diff --git a/painter.go b/painter.go index a0f81ed..71d205f 100644 --- a/painter.go +++ b/painter.go @@ -71,6 +71,9 @@ type MultiTextOption struct { Unit int Position string Align string + // The text rotation of label + TextRotation float64 + Offset Box } type GridOption struct { @@ -682,10 +685,13 @@ func (p *Painter) MultiText(opt MultiTextOption) *Painter { } else { values = autoDivide(width, count) } + offset := opt.Offset for index, text := range opt.TextList { if opt.Unit != 0 && index%opt.Unit != showIndex { continue } + p.ClearTextRotation() + p.SetTextRotation(opt.TextRotation) box := p.MeasureText(text) start := values[index] if positionCenter { @@ -706,8 +712,11 @@ func (p *Painter) MultiText(opt MultiTextOption) *Painter { } else { x = start - box.Width()>>1 } + x += offset.Left + y += offset.Top p.Text(text, x, y) } + p.ClearTextRotation() return p } diff --git a/xaxis.go b/xaxis.go index 00636a5..95578ff 100644 --- a/xaxis.go +++ b/xaxis.go @@ -47,7 +47,11 @@ type XAxisOption struct { // The line color of axis StrokeColor Color // The color of label - FontColor Color + FontColor Color + // The text rotation of label + TextRotation float64 + // The offset of label + LabelOffset Box isValueAxis bool } @@ -81,6 +85,8 @@ func (opt *XAxisOption) ToAxisOption() AxisOption { FontColor: opt.FontColor, Show: opt.Show, SplitLineColor: opt.Theme.GetAxisSplitLineColor(), + TextRotation: opt.TextRotation, + LabelOffset: opt.LabelOffset, } if opt.isValueAxis { axisOpt.SplitLineShow = true From 830d4bdd21201985bba34404086e7fbdcf8134fd Mon Sep 17 00:00:00 2001 From: vicanso Date: Sun, 11 Dec 2022 14:59:37 +0800 Subject: [PATCH 087/116] fix: fix test for text roration --- painter.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/painter.go b/painter.go index 71d205f..8f43940 100644 --- a/painter.go +++ b/painter.go @@ -685,13 +685,16 @@ func (p *Painter) MultiText(opt MultiTextOption) *Painter { } else { values = autoDivide(width, count) } + isTextRotation := opt.TextRotation != 0 offset := opt.Offset for index, text := range opt.TextList { if opt.Unit != 0 && index%opt.Unit != showIndex { continue } - p.ClearTextRotation() - p.SetTextRotation(opt.TextRotation) + if isTextRotation { + p.ClearTextRotation() + p.SetTextRotation(opt.TextRotation) + } box := p.MeasureText(text) start := values[index] if positionCenter { @@ -716,7 +719,9 @@ func (p *Painter) MultiText(opt MultiTextOption) *Painter { y += offset.Top p.Text(text, x, y) } - p.ClearTextRotation() + if isTextRotation { + p.ClearTextRotation() + } return p } From a767b3e1af4fc0a275b97a68483292ae53445a54 Mon Sep 17 00:00:00 2001 From: Thomas Knierim Date: Mon, 26 Dec 2022 15:06:53 +0700 Subject: [PATCH 088/116] added option for line chart bg fill opacity --- chart_option.go | 2 ++ charts.go | 1 + line_chart.go | 8 +++++++- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/chart_option.go b/chart_option.go index ee6851f..5311d50 100644 --- a/chart_option.go +++ b/chart_option.go @@ -71,6 +71,8 @@ type ChartOption struct { BarHeight int // Fill the area of line chart FillArea bool + // background fill (alpha) opacity + Opacity uint8 // The child charts Children []ChartOption // The value formatter diff --git a/charts.go b/charts.go index d6745d3..8613050 100644 --- a/charts.go +++ b/charts.go @@ -409,6 +409,7 @@ func Render(opt ChartOption, opts ...OptionFunc) (*Painter, error) { SymbolShow: opt.SymbolShow, StrokeWidth: opt.LineStrokeWidth, FillArea: opt.FillArea, + Opacity: opt.Opacity, }).render(renderResult, lineSeriesList) return err }) diff --git a/line_chart.go b/line_chart.go index 9f350bd..bdbd38e 100644 --- a/line_chart.go +++ b/line_chart.go @@ -70,6 +70,8 @@ type LineChartOption struct { FillArea bool // background is filled backgroundIsFilled bool + // background fill (alpha) opacity + Opacity uint8 } func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) { @@ -156,6 +158,10 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) ( areaPoints := make([]Point, len(points)) copy(areaPoints, points) bottomY := yRange.getRestHeight(yRange.min) + var opacity uint8 = 200 + if opt.Opacity != 0 { + opacity = opt.Opacity + } areaPoints = append(areaPoints, Point{ X: areaPoints[len(areaPoints)-1].X, Y: bottomY, @@ -164,7 +170,7 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) ( Y: bottomY, }, areaPoints[0]) seriesPainter.SetDrawingStyle(Style{ - FillColor: seriesColor.WithAlpha(200), + FillColor: seriesColor.WithAlpha(opacity), }) seriesPainter.FillArea(areaPoints) } From e10175594b517f9a12217478b440faecc8d3c455 Mon Sep 17 00:00:00 2001 From: vicanso Date: Thu, 5 Jan 2023 19:15:58 +0800 Subject: [PATCH 089/116] feat: support label format for funnel chart, #41 --- funnel_chart.go | 8 +++----- series.go | 8 ++++++++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/funnel_chart.go b/funnel_chart.go index 719853a..300b539 100644 --- a/funnel_chart.go +++ b/funnel_chart.go @@ -23,9 +23,6 @@ package charts import ( - "fmt" - - "github.com/dustin/go-humanize" "github.com/golang/freetype/truetype" ) @@ -95,13 +92,14 @@ func (f *funnelChart) render(result *defaultRenderResult, seriesList SeriesList) y := 0 widthList := make([]int, len(seriesList)) textList := make([]string, len(seriesList)) + seriesNames := seriesList.Names() for index, item := range seriesList { value := item.Data[0].Value widthPercent := (value - min) / (max - min) w := int(widthPercent * float64(width)) widthList[index] = w - p := humanize.CommafWithDigits(value/max*100, 2) + "%" - textList[index] = fmt.Sprintf("%s(%s)", item.Name, p) + percent := value / max + textList[index] = NewFunnelLabelFormatter(seriesNames, item.Label.Formatter)(index, value, percent) } for index, w := range widthList { diff --git a/series.go b/series.go index 13c637e..f28bfa9 100644 --- a/series.go +++ b/series.go @@ -279,6 +279,14 @@ func NewPieLabelFormatter(seriesNames []string, layout string) LabelFormatter { return NewLabelFormatter(seriesNames, layout) } +// NewFunnelLabelFormatter returns a funner label formatter +func NewFunnelLabelFormatter(seriesNames []string, layout string) LabelFormatter { + if len(layout) == 0 { + layout = "{b}({d})" + } + return NewLabelFormatter(seriesNames, layout) +} + // NewValueLabelFormatter returns a value formatter func NewValueLabelFormatter(seriesNames []string, layout string) LabelFormatter { if len(layout) == 0 { From 8ba9e2e1b207e0ead3826ba28b3bbd15304651e8 Mon Sep 17 00:00:00 2001 From: vicanso Date: Wed, 11 Jan 2023 20:41:16 +0800 Subject: [PATCH 090/116] fix: fix x axis label of horizontal bar chart, #42 --- charts.go | 11 ++++++++++- examples/horizontal_bar_chart/main.go | 28 +++++++++++++-------------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/charts.go b/charts.go index 8613050..74db733 100644 --- a/charts.go +++ b/charts.go @@ -215,7 +215,16 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e yAxisOption.Data = r.Values() } else { yAxisOption.isCategoryAxis = true - opt.XAxis.Data = r.Values() + // 由于x轴为value部分,因此计算其label单独处理 + opt.XAxis.Data = NewRange(AxisRangeOption{ + Painter: p, + Min: min, + Max: max, + // 高度需要减去x轴的高度 + Size: rangeHeight, + // 分隔数量 + DivideCount: defaultAxisDivideCount, + }).Values() opt.XAxis.isValueAxis = true } reverseStringSlice(yAxisOption.Data) diff --git a/examples/horizontal_bar_chart/main.go b/examples/horizontal_bar_chart/main.go index a0f5bda..a1c50a7 100644 --- a/examples/horizontal_bar_chart/main.go +++ b/examples/horizontal_bar_chart/main.go @@ -26,22 +26,22 @@ func writeFile(buf []byte) error { func main() { values := [][]float64{ { - 8203, - 18203, - 23489, - 29034, - 104970, - 131744, - 630230, + 10, + 30, + 50, + 70, + 90, + 110, + 130, }, { - 9325, - 19325, - 23438, - 31000, - 121594, - 134141, - 681807, + 20, + 40, + 60, + 80, + 100, + 120, + 140, }, } p, err := charts.HorizontalBarRender( From d3f7a773afc152f3be5b6237baa23bd2f40177db Mon Sep 17 00:00:00 2001 From: vicanso Date: Thu, 12 Jan 2023 20:20:36 +0800 Subject: [PATCH 091/116] fix: fix zero value of funnel chart, #43 --- examples/funnel_chart/main.go | 4 ++++ funnel_chart.go | 13 +++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/examples/funnel_chart/main.go b/examples/funnel_chart/main.go index 8f21db6..24f8afe 100644 --- a/examples/funnel_chart/main.go +++ b/examples/funnel_chart/main.go @@ -30,6 +30,8 @@ func main() { 60, 40, 20, + 10, + 0, } p, err := charts.FunnelRender( values, @@ -40,6 +42,8 @@ func main() { "Visit", "Inquiry", "Order", + "Pay", + "Cancel", }), ) if err != nil { diff --git a/funnel_chart.go b/funnel_chart.go index 300b539..d4a8bdd 100644 --- a/funnel_chart.go +++ b/funnel_chart.go @@ -93,12 +93,21 @@ func (f *funnelChart) render(result *defaultRenderResult, seriesList SeriesList) widthList := make([]int, len(seriesList)) textList := make([]string, len(seriesList)) seriesNames := seriesList.Names() + offset := max - min for index, item := range seriesList { value := item.Data[0].Value - widthPercent := (value - min) / (max - min) + // 最大最小值一致则为100% + widthPercent := 100.0 + if offset != 0 { + widthPercent = (value - min) / offset + } w := int(widthPercent * float64(width)) widthList[index] = w - percent := value / max + // 如果最大值为0,则占比100% + percent := 1.0 + if max != 0 { + percent = value / max + } textList[index] = NewFunnelLabelFormatter(seriesNames, item.Label.Formatter)(index, value, percent) } From 29a5ece5458638b95f3d85218f96be56abdadb0d Mon Sep 17 00:00:00 2001 From: vicanso Date: Tue, 14 Feb 2023 20:35:54 +0800 Subject: [PATCH 092/116] chore: update go modules --- .github/workflows/test.yml | 1 + go.mod | 6 +++--- go.sum | 13 ++++++------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 61449a3..f591a3a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,6 +14,7 @@ jobs: strategy: matrix: go: + - '1.20' - '1.19' - '1.18' - '1.17' diff --git a/go.mod b/go.mod index de0bb9c..e265627 100644 --- a/go.mod +++ b/go.mod @@ -3,15 +3,15 @@ module github.com/vicanso/go-charts/v2 go 1.17 require ( - github.com/dustin/go-humanize v1.0.0 + github.com/dustin/go-humanize v1.0.1 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 - github.com/stretchr/testify v1.8.0 + github.com/stretchr/testify v1.8.1 github.com/wcharczuk/go-chart/v2 v2.1.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/image v0.0.0-20220902085622-e7cb96979f69 // indirect + golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index e0b1547..ef2a000 100644 --- a/go.sum +++ b/go.sum @@ -1,25 +1,24 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/wcharczuk/go-chart/v2 v2.1.0 h1:tY2slqVQ6bN+yHSnDYwZebLQFkphK4WNrVwnt7CJZ2I= github.com/wcharczuk/go-chart/v2 v2.1.0/go.mod h1:yx7MvAVNcP/kN9lKXM/NTce4au4DFN99j6i1OwDclNA= +golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 h1:QelT11PB4FXiDEXucrfNckHoFxwt8USGY1ajP1ZF5lM= golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20220902085622-e7cb96979f69 h1:Lj6HJGCSn5AjxRAH2+r35Mir4icalbqku+CLUtjnvXY= -golang.org/x/image v0.0.0-20220902085622-e7cb96979f69/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 20e8d4a078b7b17d373aea3a75e95d4119b5c12f Mon Sep 17 00:00:00 2001 From: vicanso Date: Sat, 25 Feb 2023 14:04:30 +0800 Subject: [PATCH 093/116] feat: support to set the first axis --- axis.go | 4 ++ examples/time_line_chart/main.go | 82 ++++++++++++++++++++++++++++++++ painter.go | 15 +++++- xaxis.go | 3 ++ 4 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 examples/time_line_chart/main.go diff --git a/axis.go b/axis.go index 3f71451..762a6a2 100644 --- a/axis.go +++ b/axis.go @@ -63,6 +63,8 @@ type AxisOption struct { StrokeWidth float64 // The length of the axis tick TickLength int + // The first axis + FirstAxis int // The margin value of label LabelMargin int // The font size of label @@ -255,6 +257,7 @@ func (a *axisPainter) Render() (Box, error) { Length: tickLength, Unit: unit, Orient: orient, + First: opt.FirstAxis, }) p.LineStroke([]Point{ { @@ -273,6 +276,7 @@ func (a *axisPainter) Render() (Box, error) { Top: labelPaddingTop, Right: labelPaddingRight, })).MultiText(MultiTextOption{ + First: opt.FirstAxis, Align: textAlign, TextList: data, Orient: orient, diff --git a/examples/time_line_chart/main.go b/examples/time_line_chart/main.go new file mode 100644 index 0000000..10932cd --- /dev/null +++ b/examples/time_line_chart/main.go @@ -0,0 +1,82 @@ +package main + +import ( + "crypto/rand" + "fmt" + "io/ioutil" + "math/big" + "os" + "path/filepath" + "time" + + "github.com/vicanso/go-charts/v2" +) + +func writeFile(buf []byte) error { + tmpPath := "./tmp" + err := os.MkdirAll(tmpPath, 0700) + if err != nil { + return err + } + + file := filepath.Join(tmpPath, "time-line-chart.png") + err = ioutil.WriteFile(file, buf, 0600) + if err != nil { + return err + } + return nil +} + +func main() { + xAxisValue := []string{} + values := []float64{} + now := time.Now() + firstAxis := 0 + for i := 0; i < 300; i++ { + // 设置首个axis为xx:00的时间点 + if firstAxis == 0 && now.Minute() == 0 { + firstAxis = i + } + xAxisValue = append(xAxisValue, now.Format("15:04")) + now = now.Add(time.Minute) + value, _ := rand.Int(rand.Reader, big.NewInt(100)) + values = append(values, float64(value.Int64())) + } + p, err := charts.LineRender( + [][]float64{ + values, + }, + charts.TitleTextOptionFunc("Line"), + charts.XAxisDataOptionFunc(xAxisValue, charts.FalseFlag()), + charts.LegendLabelsOptionFunc([]string{ + "Demo", + }, "50"), + func(opt *charts.ChartOption) { + opt.XAxis.FirstAxis = firstAxis + // 必须要比计算得来的最小值更大(每60分钟) + opt.XAxis.SplitNumber = 60 + opt.Legend.Padding = charts.Box{ + Top: 5, + Bottom: 10, + } + opt.SymbolShow = charts.FalseFlag() + opt.LineStrokeWidth = 1 + opt.ValueFormatter = func(f float64) string { + return fmt.Sprintf("%.0f", f) + } + }, + ) + + if err != nil { + panic(err) + } + + buf, err := p.Bytes() + if err != nil { + panic(err) + } + err = writeFile(buf) + if err != nil { + panic(err) + } +} diff --git a/painter.go b/painter.go index 8f43940..18496fd 100644 --- a/painter.go +++ b/painter.go @@ -59,6 +59,8 @@ type PainterOptions struct { type PainterOption func(*Painter) type TicksOption struct { + // the first tick + First int Length int Orient string Count int @@ -74,6 +76,8 @@ type MultiTextOption struct { // The text rotation of label TextRotation float64 Offset Box + // The first text index + First int } type GridOption struct { @@ -616,6 +620,7 @@ func (p *Painter) Ticks(opt TicksOption) *Painter { return p } count := opt.Count + first := opt.First width := p.Width() height := p.Height() unit := 1 @@ -630,7 +635,10 @@ func (p *Painter) Ticks(opt TicksOption) *Painter { values = autoDivide(width, count) } for index, value := range values { - if index%unit != 0 { + if index < first { + continue + } + if (index-first)%unit != 0 { continue } if isVertical { @@ -688,7 +696,10 @@ func (p *Painter) MultiText(opt MultiTextOption) *Painter { isTextRotation := opt.TextRotation != 0 offset := opt.Offset for index, text := range opt.TextList { - if opt.Unit != 0 && index%opt.Unit != showIndex { + if index < opt.First { + continue + } + if opt.Unit != 0 && (index-opt.First)%opt.Unit != showIndex { continue } if isTextRotation { diff --git a/xaxis.go b/xaxis.go index 95578ff..61698d7 100644 --- a/xaxis.go +++ b/xaxis.go @@ -50,6 +50,8 @@ type XAxisOption struct { FontColor Color // The text rotation of label TextRotation float64 + // The first axis + FirstAxis int // The offset of label LabelOffset Box isValueAxis bool @@ -87,6 +89,7 @@ func (opt *XAxisOption) ToAxisOption() AxisOption { SplitLineColor: opt.Theme.GetAxisSplitLineColor(), TextRotation: opt.TextRotation, LabelOffset: opt.LabelOffset, + FirstAxis: opt.FirstAxis, } if opt.isValueAxis { axisOpt.SplitLineShow = true From e7a49c2c212d022df6cda4739b8c3e391fd3594a Mon Sep 17 00:00:00 2001 From: Mike Jensen Date: Thu, 4 May 2023 12:52:28 -0600 Subject: [PATCH 094/116] Improvements to how the X Axis is rendered This provides two improvements to how the X Axis is rendered: * The calculation for where a tick should exist has been improved. It now will ensure a tick is always at both the start of the axis and the end of the axis. This makes it clear exactly what data span is captured in the graph. * The second improvement is how the label on the last tick is written. It used to often get partially cut off, and with the change to ensure a tick is always at the end this could be seen more easily. Now the last tick has it's label written to the left so that it can be fully displayed. --- painter.go | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/painter.go b/painter.go index 18496fd..450afdf 100644 --- a/painter.go +++ b/painter.go @@ -615,6 +615,17 @@ func (p *Painter) TextFit(body string, x, y, width int, textAligns ...string) ch return output } +func isTick(totalRange int, numTicks int, index int) bool { + step := float64(totalRange-1) / float64(numTicks-1) + for i := 0; i < numTicks; i++ { + value := float64(i) * step + if int(value + 0.5) == index { + return true + } + } + return false +} + func (p *Painter) Ticks(opt TicksOption) *Painter { if opt.Count <= 0 || opt.Length <= 0 { return p @@ -638,7 +649,7 @@ func (p *Painter) Ticks(opt TicksOption) *Painter { if index < first { continue } - if (index-first)%unit != 0 { + if ! isTick(len(values), unit, index) { continue } if isVertical { @@ -674,15 +685,13 @@ func (p *Painter) MultiText(opt MultiTextOption) *Painter { } count := len(opt.TextList) positionCenter := true - showIndex := opt.Unit / 2 + tickLimit := true if containsString([]string{ PositionLeft, PositionTop, }, opt.Position) { positionCenter = false count-- - // 非居中 - showIndex = 0 } width := p.Width() height := p.Height() @@ -690,6 +699,7 @@ func (p *Painter) MultiText(opt MultiTextOption) *Painter { isVertical := opt.Orient == OrientVertical if isVertical { values = autoDivide(height, count) + tickLimit = false } else { values = autoDivide(width, count) } @@ -699,7 +709,7 @@ func (p *Painter) MultiText(opt MultiTextOption) *Painter { if index < opt.First { continue } - if opt.Unit != 0 && (index-opt.First)%opt.Unit != showIndex { + if opt.Unit != 0 && tickLimit && ! isTick(len(opt.TextList)-opt.First, opt.Unit, index-opt.First) { continue } if isTextRotation { @@ -724,7 +734,11 @@ func (p *Painter) MultiText(opt MultiTextOption) *Painter { x = 0 } } else { - x = start - box.Width()>>1 + if index == len(opt.TextList) - 1 { + x = start - box.Width() + } else { + x = start - box.Width()>>1 + } } x += offset.Left y += offset.Top @@ -749,7 +763,6 @@ func (p *Painter) Grid(opt GridOption) *Painter { x1 := 0 y1 := 0 if isVertical { - x0 = v x1 = v y1 = height From 19173dfd37f737b4a5a681556b0a7fe661d749db Mon Sep 17 00:00:00 2001 From: Mike Jensen Date: Thu, 4 May 2023 17:59:11 -0600 Subject: [PATCH 095/116] painter.go: Optimize isTick function This reduces the loop frequency to one or two iterations in all cases. I have been unable to find any single line equation that can produce this same behavior, but one likely exists. --- painter.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/painter.go b/painter.go index 450afdf..175b2f2 100644 --- a/painter.go +++ b/painter.go @@ -617,10 +617,12 @@ func (p *Painter) TextFit(body string, x, y, width int, textAligns ...string) ch func isTick(totalRange int, numTicks int, index int) bool { step := float64(totalRange-1) / float64(numTicks-1) - for i := 0; i < numTicks; i++ { - value := float64(i) * step - if int(value + 0.5) == index { + for i := int(float64(index) / step); i < numTicks; i++ { + value := int((float64(i) * step) + 0.5) + if value == index { return true + } else if value > index { + break } } return false From c810369730585ed5692c3e5e13c36a4c785568c1 Mon Sep 17 00:00:00 2001 From: Mike Jensen Date: Fri, 5 May 2023 09:44:09 -0600 Subject: [PATCH 096/116] Change ticks to avoid values impacting each other The recently introduced logic has an incorrect understanding of the `unit` parameter. This would result in too many ticks being outputted, particularly as datasets got larger. This fixes it by re-calculating the tick count using the `unit` param as originally intended. --- painter.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/painter.go b/painter.go index 175b2f2..d74b80d 100644 --- a/painter.go +++ b/painter.go @@ -615,7 +615,8 @@ func (p *Painter) TextFit(body string, x, y, width int, textAligns ...string) ch return output } -func isTick(totalRange int, numTicks int, index int) bool { +func isTick(totalRange int, unit int, index int) bool { + numTicks := (totalRange / unit) + 1 step := float64(totalRange-1) / float64(numTicks-1) for i := int(float64(index) / step); i < numTicks; i++ { value := int((float64(i) * step) + 0.5) @@ -737,7 +738,7 @@ func (p *Painter) MultiText(opt MultiTextOption) *Painter { } } else { if index == len(opt.TextList) - 1 { - x = start - box.Width() + x = start - box.Width() + 10 } else { x = start - box.Width()>>1 } From a158191faf90eb0baa08d2cbe8692afd5b09b58c Mon Sep 17 00:00:00 2001 From: Mike Jensen Date: Fri, 5 May 2023 09:55:55 -0600 Subject: [PATCH 097/116] Add `Unit` to XAxis as a publicly visible parameter In some cases the XAxis may have a single long title. This can result in very few increments being shown. In order to be more flexible for those cases this allows the XAxis Tick frequency to be able to be directly controlled. --- axis.go | 1 - xaxis.go | 5 ++++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/axis.go b/axis.go index 762a6a2..af104a8 100644 --- a/axis.go +++ b/axis.go @@ -176,7 +176,6 @@ func (a *axisPainter) Render() (Box, error) { unit := opt.Unit if unit <= 0 { - unit = ceilFloatToInt(float64(dataCount) / float64(fitTextCount)) unit = chart.MaxInt(unit, opt.SplitNumber) // 偶数 diff --git a/xaxis.go b/xaxis.go index 61698d7..5557015 100644 --- a/xaxis.go +++ b/xaxis.go @@ -40,7 +40,7 @@ type XAxisOption struct { FontSize float64 // The flag for show axis, set this to *false will hide axis Show *bool - // Number of segments that the axis is split into. Note that this number serves only as a recommendation. + // Number of segments that the axis is split into. Note that this number serves only as a recommendation to avoid writing overlap. SplitNumber int // The position of axis, it can be 'top' or 'bottom' Position string @@ -55,6 +55,8 @@ type XAxisOption struct { // The offset of label LabelOffset Box isValueAxis bool + // This value overrides SplitNumber, specifying directly the frequency at which the axis is split into, higher numbers result in less ticks + Unit int } const defaultXAxisHeight = 30 @@ -90,6 +92,7 @@ func (opt *XAxisOption) ToAxisOption() AxisOption { TextRotation: opt.TextRotation, LabelOffset: opt.LabelOffset, FirstAxis: opt.FirstAxis, + Unit: opt.Unit, } if opt.isValueAxis { axisOpt.SplitLineShow = true From 687baad0af8ff907e1e317f2671e3d76b91746c1 Mon Sep 17 00:00:00 2001 From: Mike Jensen Date: Fri, 5 May 2023 10:19:01 -0600 Subject: [PATCH 098/116] Unit test fixes Unit tests updated for new tick positions and in a couple cases additional one X axis sample. --- axis_test.go | 6 +++--- bar_chart_test.go | 2 +- chart_option_test.go | 6 +++--- echarts_test.go | 2 +- horizontal_bar_chart_test.go | 2 +- line_chart_test.go | 4 ++-- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/axis_test.go b/axis_test.go index d0cff41..a04024d 100644 --- a/axis_test.go +++ b/axis_test.go @@ -53,7 +53,7 @@ func TestAxis(t *testing.T) { }).Render() return p.Bytes() }, - result: "\\nMonTueWedThuFriSatSun", + result: "\\nMonTueWedThuFriSatSun", }, // 底部x轴文本居左 { @@ -72,7 +72,7 @@ func TestAxis(t *testing.T) { }).Render() return p.Bytes() }, - result: "\\nMonTueWedThuFriSatSun", + result: "\\nMonTueWedThuFriSatSun", }, // 左侧y轴 { @@ -155,7 +155,7 @@ func TestAxis(t *testing.T) { }).Render() return p.Bytes() }, - result: "\\nMon --Tue --Wed --Thu --Fri --Sat --Sun --", + result: "\\nMon --Tue --Wed --Thu --Fri --Sat --Sun --", }, } diff --git a/bar_chart_test.go b/bar_chart_test.go index e1522d6..aec6428 100644 --- a/bar_chart_test.go +++ b/bar_chart_test.go @@ -102,7 +102,7 @@ func TestBarChart(t *testing.T) { } return p.Bytes() }, - result: "\\n24020016012080400FebMayAugNov24.9723.225.676.7135.6162.232.6206.43.32.65.9926.428.770.7175.6182.248.718.862.3", + result: "\\n24020016012080400JanAprJulSepDec24.9723.225.676.7135.6162.232.6206.43.32.65.9926.428.770.7175.6182.248.718.862.3", }, } diff --git a/chart_option_test.go b/chart_option_test.go index ff17750..b7f4e93 100644 --- a/chart_option_test.go +++ b/chart_option_test.go @@ -204,7 +204,7 @@ func TestLineRender(t *testing.T) { assert.Nil(err) data, err := p.Bytes() assert.Nil(err) - assert.Equal("\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", string(data)) + assert.Equal("\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", string(data)) } func TestBarRender(t *testing.T) { @@ -277,7 +277,7 @@ func TestBarRender(t *testing.T) { assert.Nil(err) data, err := p.Bytes() assert.Nil(err) - assert.Equal("\\nRainfallEvaporation24020016012080400FebMayAugNov162.22182.22.341.6248.07", string(data)) + assert.Equal("\\nRainfallEvaporation24020016012080400JanAprJulSepDec162.22182.22.341.6248.07", string(data)) } func TestHorizontalBarRender(t *testing.T) { @@ -326,7 +326,7 @@ func TestHorizontalBarRender(t *testing.T) { assert.Nil(err) data, err := p.Bytes() assert.Nil(err) - assert.Equal("\\n20112012World PopulationWorldChinaIndiaUSAIndonesiaBrazil0122.28k244.56k366.84k489.12k611.4k733.68k", string(data)) + assert.Equal("\\n20112012World PopulationWorldChinaIndiaUSAIndonesiaBrazil0122.28k244.56k366.84k489.12k611.4k733.68k", string(data)) } func TestPieRender(t *testing.T) { diff --git a/echarts_test.go b/echarts_test.go index 2ce1715..dd7562f 100644 --- a/echarts_test.go +++ b/echarts_test.go @@ -578,5 +578,5 @@ func TestRenderEChartsToSVG(t *testing.T) { ] }`) assert.Nil(err) - assert.Equal("\\nRainfallEvaporationRainfall vs EvaporationFake Data24020016012080400FebMayAugNov162.22182.22.341.6248.07", string(data)) + assert.Equal("\\nRainfallEvaporationRainfall vs EvaporationFake Data24020016012080400JanAprJulSepDec162.22182.22.341.6248.07", string(data)) } diff --git a/horizontal_bar_chart_test.go b/horizontal_bar_chart_test.go index e078c4a..78f3e69 100644 --- a/horizontal_bar_chart_test.go +++ b/horizontal_bar_chart_test.go @@ -83,7 +83,7 @@ func TestHorizontalBarChart(t *testing.T) { } return p.Bytes() }, - result: "\\n20112012World PopulationWorldChinaIndiaUSAIndonesiaBrazil0122.28k244.56k366.84k489.12k611.4k733.68k", + result: "\\n20112012World PopulationWorldChinaIndiaUSAIndonesiaBrazil0122.28k244.56k366.84k489.12k611.4k733.68k", }, } for _, tt := range tests { diff --git a/line_chart_test.go b/line_chart_test.go index e169f90..e8bc1d7 100644 --- a/line_chart_test.go +++ b/line_chart_test.go @@ -117,7 +117,7 @@ func TestLineChart(t *testing.T) { } return p.Bytes() }, - result: "\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", + result: "\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", }, { render: func(p *Painter) ([]byte, error) { @@ -201,7 +201,7 @@ func TestLineChart(t *testing.T) { } return p.Bytes() }, - result: "\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", + result: "\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", }, } From 0ddb9e4ef1d089a08fdf712377e6d3d7d7396cc4 Mon Sep 17 00:00:00 2001 From: vicanso Date: Fri, 12 May 2023 20:31:42 +0800 Subject: [PATCH 099/116] chore: update modules --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index e265627..d8a492c 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.17 require ( github.com/dustin/go-humanize v1.0.1 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 - github.com/stretchr/testify v1.8.1 + github.com/stretchr/testify v1.8.2 github.com/wcharczuk/go-chart/v2 v2.1.0 ) diff --git a/go.sum b/go.sum index ef2a000..ac1d9f7 100644 --- a/go.sum +++ b/go.sum @@ -12,8 +12,8 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/wcharczuk/go-chart/v2 v2.1.0 h1:tY2slqVQ6bN+yHSnDYwZebLQFkphK4WNrVwnt7CJZ2I= github.com/wcharczuk/go-chart/v2 v2.1.0/go.mod h1:yx7MvAVNcP/kN9lKXM/NTce4au4DFN99j6i1OwDclNA= golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 h1:QelT11PB4FXiDEXucrfNckHoFxwt8USGY1ajP1ZF5lM= From 8bcb584abac4fe128bd83103a9b3a04a01db0346 Mon Sep 17 00:00:00 2001 From: Tree Xie Date: Wed, 27 Dec 2023 18:20:55 +0800 Subject: [PATCH 100/116] Revert "Improvements to how the X Axis is rendered" --- axis.go | 1 + axis_test.go | 6 +++--- bar_chart_test.go | 2 +- chart_option_test.go | 6 +++--- echarts_test.go | 2 +- horizontal_bar_chart_test.go | 2 +- line_chart_test.go | 4 ++-- painter.go | 30 +++++++----------------------- xaxis.go | 5 +---- 9 files changed, 20 insertions(+), 38 deletions(-) diff --git a/axis.go b/axis.go index af104a8..762a6a2 100644 --- a/axis.go +++ b/axis.go @@ -176,6 +176,7 @@ func (a *axisPainter) Render() (Box, error) { unit := opt.Unit if unit <= 0 { + unit = ceilFloatToInt(float64(dataCount) / float64(fitTextCount)) unit = chart.MaxInt(unit, opt.SplitNumber) // 偶数 diff --git a/axis_test.go b/axis_test.go index a04024d..d0cff41 100644 --- a/axis_test.go +++ b/axis_test.go @@ -53,7 +53,7 @@ func TestAxis(t *testing.T) { }).Render() return p.Bytes() }, - result: "\\nMonTueWedThuFriSatSun", + result: "\\nMonTueWedThuFriSatSun", }, // 底部x轴文本居左 { @@ -72,7 +72,7 @@ func TestAxis(t *testing.T) { }).Render() return p.Bytes() }, - result: "\\nMonTueWedThuFriSatSun", + result: "\\nMonTueWedThuFriSatSun", }, // 左侧y轴 { @@ -155,7 +155,7 @@ func TestAxis(t *testing.T) { }).Render() return p.Bytes() }, - result: "\\nMon --Tue --Wed --Thu --Fri --Sat --Sun --", + result: "\\nMon --Tue --Wed --Thu --Fri --Sat --Sun --", }, } diff --git a/bar_chart_test.go b/bar_chart_test.go index aec6428..e1522d6 100644 --- a/bar_chart_test.go +++ b/bar_chart_test.go @@ -102,7 +102,7 @@ func TestBarChart(t *testing.T) { } return p.Bytes() }, - result: "\\n24020016012080400JanAprJulSepDec24.9723.225.676.7135.6162.232.6206.43.32.65.9926.428.770.7175.6182.248.718.862.3", + result: "\\n24020016012080400FebMayAugNov24.9723.225.676.7135.6162.232.6206.43.32.65.9926.428.770.7175.6182.248.718.862.3", }, } diff --git a/chart_option_test.go b/chart_option_test.go index b7f4e93..ff17750 100644 --- a/chart_option_test.go +++ b/chart_option_test.go @@ -204,7 +204,7 @@ func TestLineRender(t *testing.T) { assert.Nil(err) data, err := p.Bytes() assert.Nil(err) - assert.Equal("\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", string(data)) + assert.Equal("\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", string(data)) } func TestBarRender(t *testing.T) { @@ -277,7 +277,7 @@ func TestBarRender(t *testing.T) { assert.Nil(err) data, err := p.Bytes() assert.Nil(err) - assert.Equal("\\nRainfallEvaporation24020016012080400JanAprJulSepDec162.22182.22.341.6248.07", string(data)) + assert.Equal("\\nRainfallEvaporation24020016012080400FebMayAugNov162.22182.22.341.6248.07", string(data)) } func TestHorizontalBarRender(t *testing.T) { @@ -326,7 +326,7 @@ func TestHorizontalBarRender(t *testing.T) { assert.Nil(err) data, err := p.Bytes() assert.Nil(err) - assert.Equal("\\n20112012World PopulationWorldChinaIndiaUSAIndonesiaBrazil0122.28k244.56k366.84k489.12k611.4k733.68k", string(data)) + assert.Equal("\\n20112012World PopulationWorldChinaIndiaUSAIndonesiaBrazil0122.28k244.56k366.84k489.12k611.4k733.68k", string(data)) } func TestPieRender(t *testing.T) { diff --git a/echarts_test.go b/echarts_test.go index dd7562f..2ce1715 100644 --- a/echarts_test.go +++ b/echarts_test.go @@ -578,5 +578,5 @@ func TestRenderEChartsToSVG(t *testing.T) { ] }`) assert.Nil(err) - assert.Equal("\\nRainfallEvaporationRainfall vs EvaporationFake Data24020016012080400JanAprJulSepDec162.22182.22.341.6248.07", string(data)) + assert.Equal("\\nRainfallEvaporationRainfall vs EvaporationFake Data24020016012080400FebMayAugNov162.22182.22.341.6248.07", string(data)) } diff --git a/horizontal_bar_chart_test.go b/horizontal_bar_chart_test.go index 78f3e69..e078c4a 100644 --- a/horizontal_bar_chart_test.go +++ b/horizontal_bar_chart_test.go @@ -83,7 +83,7 @@ func TestHorizontalBarChart(t *testing.T) { } return p.Bytes() }, - result: "\\n20112012World PopulationWorldChinaIndiaUSAIndonesiaBrazil0122.28k244.56k366.84k489.12k611.4k733.68k", + result: "\\n20112012World PopulationWorldChinaIndiaUSAIndonesiaBrazil0122.28k244.56k366.84k489.12k611.4k733.68k", }, } for _, tt := range tests { diff --git a/line_chart_test.go b/line_chart_test.go index e8bc1d7..e169f90 100644 --- a/line_chart_test.go +++ b/line_chart_test.go @@ -117,7 +117,7 @@ func TestLineChart(t *testing.T) { } return p.Bytes() }, - result: "\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", + result: "\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", }, { render: func(p *Painter) ([]byte, error) { @@ -201,7 +201,7 @@ func TestLineChart(t *testing.T) { } return p.Bytes() }, - result: "\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", + result: "\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", }, } diff --git a/painter.go b/painter.go index d74b80d..18496fd 100644 --- a/painter.go +++ b/painter.go @@ -615,20 +615,6 @@ func (p *Painter) TextFit(body string, x, y, width int, textAligns ...string) ch return output } -func isTick(totalRange int, unit int, index int) bool { - numTicks := (totalRange / unit) + 1 - step := float64(totalRange-1) / float64(numTicks-1) - for i := int(float64(index) / step); i < numTicks; i++ { - value := int((float64(i) * step) + 0.5) - if value == index { - return true - } else if value > index { - break - } - } - return false -} - func (p *Painter) Ticks(opt TicksOption) *Painter { if opt.Count <= 0 || opt.Length <= 0 { return p @@ -652,7 +638,7 @@ func (p *Painter) Ticks(opt TicksOption) *Painter { if index < first { continue } - if ! isTick(len(values), unit, index) { + if (index-first)%unit != 0 { continue } if isVertical { @@ -688,13 +674,15 @@ func (p *Painter) MultiText(opt MultiTextOption) *Painter { } count := len(opt.TextList) positionCenter := true - tickLimit := true + showIndex := opt.Unit / 2 if containsString([]string{ PositionLeft, PositionTop, }, opt.Position) { positionCenter = false count-- + // 非居中 + showIndex = 0 } width := p.Width() height := p.Height() @@ -702,7 +690,6 @@ func (p *Painter) MultiText(opt MultiTextOption) *Painter { isVertical := opt.Orient == OrientVertical if isVertical { values = autoDivide(height, count) - tickLimit = false } else { values = autoDivide(width, count) } @@ -712,7 +699,7 @@ func (p *Painter) MultiText(opt MultiTextOption) *Painter { if index < opt.First { continue } - if opt.Unit != 0 && tickLimit && ! isTick(len(opt.TextList)-opt.First, opt.Unit, index-opt.First) { + if opt.Unit != 0 && (index-opt.First)%opt.Unit != showIndex { continue } if isTextRotation { @@ -737,11 +724,7 @@ func (p *Painter) MultiText(opt MultiTextOption) *Painter { x = 0 } } else { - if index == len(opt.TextList) - 1 { - x = start - box.Width() + 10 - } else { - x = start - box.Width()>>1 - } + x = start - box.Width()>>1 } x += offset.Left y += offset.Top @@ -766,6 +749,7 @@ func (p *Painter) Grid(opt GridOption) *Painter { x1 := 0 y1 := 0 if isVertical { + x0 = v x1 = v y1 = height diff --git a/xaxis.go b/xaxis.go index 5557015..61698d7 100644 --- a/xaxis.go +++ b/xaxis.go @@ -40,7 +40,7 @@ type XAxisOption struct { FontSize float64 // The flag for show axis, set this to *false will hide axis Show *bool - // Number of segments that the axis is split into. Note that this number serves only as a recommendation to avoid writing overlap. + // Number of segments that the axis is split into. Note that this number serves only as a recommendation. SplitNumber int // The position of axis, it can be 'top' or 'bottom' Position string @@ -55,8 +55,6 @@ type XAxisOption struct { // The offset of label LabelOffset Box isValueAxis bool - // This value overrides SplitNumber, specifying directly the frequency at which the axis is split into, higher numbers result in less ticks - Unit int } const defaultXAxisHeight = 30 @@ -92,7 +90,6 @@ func (opt *XAxisOption) ToAxisOption() AxisOption { TextRotation: opt.TextRotation, LabelOffset: opt.LabelOffset, FirstAxis: opt.FirstAxis, - Unit: opt.Unit, } if opt.isValueAxis { axisOpt.SplitLineShow = true From 98af9866a47cd7ac3665588501889601f7900ef4 Mon Sep 17 00:00:00 2001 From: vicanso Date: Wed, 27 Dec 2023 20:33:12 +0800 Subject: [PATCH 101/116] refactor: support label show for radar chart, #62 --- echarts.go | 5 +++++ radar_chart.go | 9 ++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/echarts.go b/echarts.go index fbe9a36..5a0e5a0 100644 --- a/echarts.go +++ b/echarts.go @@ -344,6 +344,11 @@ func (esList EChartsSeriesList) ToSeriesList() SeriesList { Data: NewSeriesDataFromValues(dataItem.Value.values), Max: item.Max, Min: item.Min, + Label: SeriesLabel{ + Color: parseColor(item.Label.Color), + Show: item.Label.Show, + Distance: item.Label.Distance, + }, }) } continue diff --git a/radar_chart.go b/radar_chart.go index 429850d..f3d63b9 100644 --- a/radar_chart.go +++ b/radar_chart.go @@ -25,6 +25,7 @@ package charts import ( "errors" + "github.com/dustin/go-humanize" "github.com/golang/freetype/truetype" "github.com/wcharczuk/go-chart/v2" "github.com/wcharczuk/go-chart/v2/drawing" @@ -230,9 +231,15 @@ func (r *radarChart) render(result *defaultRenderResult, seriesList SeriesList) StrokeColor: color, FillColor: dotFillColor, }) - for _, point := range linePoints { + for index, point := range linePoints { seriesPainter.Circle(dotWith, point.X, point.Y) seriesPainter.FillStroke() + if series.Label.Show && index < len(series.Data) { + value := humanize.FtoaWithDigits(series.Data[index].Value, 2) + b := seriesPainter.MeasureText(value) + seriesPainter.Text(value, point.X-b.Width()/2, point.Y) + } + } } From c2f709a74299db5a1c0290f5d6c07606f0b9f124 Mon Sep 17 00:00:00 2001 From: vicanso Date: Wed, 27 Dec 2023 20:34:05 +0800 Subject: [PATCH 102/116] chore: update modules --- .github/workflows/test.yml | 1 + go.mod | 6 +++--- go.sum | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f591a3a..8d6a027 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,6 +14,7 @@ jobs: strategy: matrix: go: + - '1.21' - '1.20' - '1.19' - '1.18' diff --git a/go.mod b/go.mod index d8a492c..b46ff02 100644 --- a/go.mod +++ b/go.mod @@ -5,13 +5,13 @@ go 1.17 require ( github.com/dustin/go-humanize v1.0.1 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 - github.com/stretchr/testify v1.8.2 - github.com/wcharczuk/go-chart/v2 v2.1.0 + github.com/stretchr/testify v1.8.4 + github.com/wcharczuk/go-chart/v2 v2.1.1 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 // indirect + golang.org/x/image v0.14.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index ac1d9f7..e518b63 100644 --- a/go.sum +++ b/go.sum @@ -14,11 +14,48 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/wcharczuk/go-chart/v2 v2.1.0 h1:tY2slqVQ6bN+yHSnDYwZebLQFkphK4WNrVwnt7CJZ2I= github.com/wcharczuk/go-chart/v2 v2.1.0/go.mod h1:yx7MvAVNcP/kN9lKXM/NTce4au4DFN99j6i1OwDclNA= +github.com/wcharczuk/go-chart/v2 v2.1.1 h1:2u7na789qiD5WzccZsFz4MJWOJP72G+2kUuJoSNqWnE= +github.com/wcharczuk/go-chart/v2 v2.1.1/go.mod h1:CyCAUt2oqvfhCl6Q5ZvAZwItgpQKZOkCJGb+VGv6l14= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 h1:QelT11PB4FXiDEXucrfNckHoFxwt8USGY1ajP1ZF5lM= golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.11.0/go.mod h1:bglhjqbqVuEb9e9+eNR45Jfu7D+T4Qan+NhQk8Ck2P8= +golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4= +golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From e09ab2c3c7a5a338be739f503361bc17779733dd Mon Sep 17 00:00:00 2001 From: vicanso Date: Wed, 27 Dec 2023 20:37:18 +0800 Subject: [PATCH 103/116] Revert "chore: update modules" This reverts commit c2f709a74299db5a1c0290f5d6c07606f0b9f124. --- .github/workflows/test.yml | 1 - go.mod | 6 +++--- go.sum | 37 ------------------------------------- 3 files changed, 3 insertions(+), 41 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8d6a027..f591a3a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,7 +14,6 @@ jobs: strategy: matrix: go: - - '1.21' - '1.20' - '1.19' - '1.18' diff --git a/go.mod b/go.mod index b46ff02..d8a492c 100644 --- a/go.mod +++ b/go.mod @@ -5,13 +5,13 @@ go 1.17 require ( github.com/dustin/go-humanize v1.0.1 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 - github.com/stretchr/testify v1.8.4 - github.com/wcharczuk/go-chart/v2 v2.1.1 + github.com/stretchr/testify v1.8.2 + github.com/wcharczuk/go-chart/v2 v2.1.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/image v0.14.0 // indirect + golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index e518b63..ac1d9f7 100644 --- a/go.sum +++ b/go.sum @@ -14,48 +14,11 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/wcharczuk/go-chart/v2 v2.1.0 h1:tY2slqVQ6bN+yHSnDYwZebLQFkphK4WNrVwnt7CJZ2I= github.com/wcharczuk/go-chart/v2 v2.1.0/go.mod h1:yx7MvAVNcP/kN9lKXM/NTce4au4DFN99j6i1OwDclNA= -github.com/wcharczuk/go-chart/v2 v2.1.1 h1:2u7na789qiD5WzccZsFz4MJWOJP72G+2kUuJoSNqWnE= -github.com/wcharczuk/go-chart/v2 v2.1.1/go.mod h1:CyCAUt2oqvfhCl6Q5ZvAZwItgpQKZOkCJGb+VGv6l14= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 h1:QelT11PB4FXiDEXucrfNckHoFxwt8USGY1ajP1ZF5lM= golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.11.0/go.mod h1:bglhjqbqVuEb9e9+eNR45Jfu7D+T4Qan+NhQk8Ck2P8= -golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4= -golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 310800a5f01eaa29226b7595cdcc0384d2d9f55b Mon Sep 17 00:00:00 2001 From: "xuejinwei.1112" Date: Tue, 2 Jan 2024 12:32:56 +0800 Subject: [PATCH 104/116] support dash line for line chart --- line_chart.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/line_chart.go b/line_chart.go index bdbd38e..363cd36 100644 --- a/line_chart.go +++ b/line_chart.go @@ -115,6 +115,9 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) ( StrokeColor: seriesColor, StrokeWidth: strokeWidth, } + if len(series.Style.StrokeDashArray) > 0 { + drawingStyle.StrokeDashArray = series.Style.StrokeDashArray + } yRange := result.axisRanges[series.AxisIndex] points := make([]Point, 0) From f1a231ff4b0e660609709babc1a9336c89c0233a Mon Sep 17 00:00:00 2001 From: vicanso Date: Sun, 11 Feb 2024 12:36:26 +0800 Subject: [PATCH 105/116] feat: support split line show option for charts, #69 --- .github/workflows/test.yml | 2 ++ examples/area_line_chart/main.go | 3 +-- examples/bar_chart/main.go | 3 +-- examples/chinese/main.go | 2 +- examples/funnel_chart/main.go | 3 +-- examples/horizontal_bar_chart/main.go | 3 +-- examples/line_chart/main.go | 8 ++++++-- examples/painter/main.go | 3 +-- examples/pie_chart/main.go | 3 +-- examples/radar_chart/main.go | 3 +-- examples/table/main.go | 3 +-- examples/time_line_chart/main.go | 3 +-- yaxis.go | 5 +++++ 13 files changed, 23 insertions(+), 21 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f591a3a..5544970 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,6 +14,8 @@ jobs: strategy: matrix: go: + - '1.22' + - '1.21' - '1.20' - '1.19' - '1.18' diff --git a/examples/area_line_chart/main.go b/examples/area_line_chart/main.go index 7a84df0..ea8f1c2 100644 --- a/examples/area_line_chart/main.go +++ b/examples/area_line_chart/main.go @@ -1,7 +1,6 @@ package main import ( - "io/ioutil" "os" "path/filepath" @@ -16,7 +15,7 @@ func writeFile(buf []byte) error { } file := filepath.Join(tmpPath, "area-line-chart.png") - err = ioutil.WriteFile(file, buf, 0600) + err = os.WriteFile(file, buf, 0600) if err != nil { return err } diff --git a/examples/bar_chart/main.go b/examples/bar_chart/main.go index c559a76..feea66e 100644 --- a/examples/bar_chart/main.go +++ b/examples/bar_chart/main.go @@ -1,7 +1,6 @@ package main import ( - "io/ioutil" "os" "path/filepath" @@ -16,7 +15,7 @@ func writeFile(buf []byte) error { } file := filepath.Join(tmpPath, "bar-chart.png") - err = ioutil.WriteFile(file, buf, 0600) + err = os.WriteFile(file, buf, 0600) if err != nil { return err } diff --git a/examples/chinese/main.go b/examples/chinese/main.go index d77216a..2d96b58 100644 --- a/examples/chinese/main.go +++ b/examples/chinese/main.go @@ -16,7 +16,7 @@ func writeFile(buf []byte) error { } file := filepath.Join(tmpPath, "chinese-line-chart.png") - err = ioutil.WriteFile(file, buf, 0600) + err = os.WriteFile(file, buf, 0600) if err != nil { return err } diff --git a/examples/funnel_chart/main.go b/examples/funnel_chart/main.go index 24f8afe..f29ccf9 100644 --- a/examples/funnel_chart/main.go +++ b/examples/funnel_chart/main.go @@ -1,7 +1,6 @@ package main import ( - "io/ioutil" "os" "path/filepath" @@ -16,7 +15,7 @@ func writeFile(buf []byte) error { } file := filepath.Join(tmpPath, "funnel-chart.png") - err = ioutil.WriteFile(file, buf, 0600) + err = os.WriteFile(file, buf, 0600) if err != nil { return err } diff --git a/examples/horizontal_bar_chart/main.go b/examples/horizontal_bar_chart/main.go index a1c50a7..f2cabe8 100644 --- a/examples/horizontal_bar_chart/main.go +++ b/examples/horizontal_bar_chart/main.go @@ -1,7 +1,6 @@ package main import ( - "io/ioutil" "os" "path/filepath" @@ -16,7 +15,7 @@ func writeFile(buf []byte) error { } file := filepath.Join(tmpPath, "horizontal-bar-chart.png") - err = ioutil.WriteFile(file, buf, 0600) + err = os.WriteFile(file, buf, 0600) if err != nil { return err } diff --git a/examples/line_chart/main.go b/examples/line_chart/main.go index c1478a6..4e6448f 100644 --- a/examples/line_chart/main.go +++ b/examples/line_chart/main.go @@ -2,7 +2,6 @@ package main import ( "fmt" - "io/ioutil" "os" "path/filepath" @@ -17,7 +16,7 @@ func writeFile(buf []byte) error { } file := filepath.Join(tmpPath, "line-chart.png") - err = ioutil.WriteFile(file, buf, 0600) + err = os.WriteFile(file, buf, 0600) if err != nil { return err } @@ -97,6 +96,11 @@ func main() { Top: 5, Bottom: 10, } + opt.YAxisOptions = []charts.YAxisOption{ + { + SplitLineShow: charts.FalseFlag(), + }, + } opt.SymbolShow = charts.FalseFlag() opt.LineStrokeWidth = 1 opt.ValueFormatter = func(f float64) string { diff --git a/examples/painter/main.go b/examples/painter/main.go index 3c31ce4..b7a5832 100644 --- a/examples/painter/main.go +++ b/examples/painter/main.go @@ -1,7 +1,6 @@ package main import ( - "io/ioutil" "os" "path/filepath" @@ -17,7 +16,7 @@ func writeFile(buf []byte) error { } file := filepath.Join(tmpPath, "painter.png") - err = ioutil.WriteFile(file, buf, 0600) + err = os.WriteFile(file, buf, 0600) if err != nil { return err } diff --git a/examples/pie_chart/main.go b/examples/pie_chart/main.go index 3721ed1..38488d2 100644 --- a/examples/pie_chart/main.go +++ b/examples/pie_chart/main.go @@ -1,7 +1,6 @@ package main import ( - "io/ioutil" "os" "path/filepath" @@ -16,7 +15,7 @@ func writeFile(buf []byte) error { } file := filepath.Join(tmpPath, "pie-chart.png") - err = ioutil.WriteFile(file, buf, 0600) + err = os.WriteFile(file, buf, 0600) if err != nil { return err } diff --git a/examples/radar_chart/main.go b/examples/radar_chart/main.go index 51f7409..e8095ae 100644 --- a/examples/radar_chart/main.go +++ b/examples/radar_chart/main.go @@ -1,7 +1,6 @@ package main import ( - "io/ioutil" "os" "path/filepath" @@ -16,7 +15,7 @@ func writeFile(buf []byte) error { } file := filepath.Join(tmpPath, "radar-chart.png") - err = ioutil.WriteFile(file, buf, 0600) + err = os.WriteFile(file, buf, 0600) if err != nil { return err } diff --git a/examples/table/main.go b/examples/table/main.go index 2701ec1..0210ecf 100644 --- a/examples/table/main.go +++ b/examples/table/main.go @@ -1,7 +1,6 @@ package main import ( - "io/ioutil" "os" "path/filepath" "strconv" @@ -19,7 +18,7 @@ func writeFile(buf []byte, filename string) error { } file := filepath.Join(tmpPath, filename) - err = ioutil.WriteFile(file, buf, 0600) + err = os.WriteFile(file, buf, 0600) if err != nil { return err } diff --git a/examples/time_line_chart/main.go b/examples/time_line_chart/main.go index 10932cd..6cb3f3d 100644 --- a/examples/time_line_chart/main.go +++ b/examples/time_line_chart/main.go @@ -3,7 +3,6 @@ package main import ( "crypto/rand" "fmt" - "io/ioutil" "math/big" "os" "path/filepath" @@ -20,7 +19,7 @@ func writeFile(buf []byte) error { } file := filepath.Join(tmpPath, "time-line-chart.png") - err = ioutil.WriteFile(file, buf, 0600) + err = os.WriteFile(file, buf, 0600) if err != nil { return err } diff --git a/yaxis.go b/yaxis.go index bece2cc..e58b7a6 100644 --- a/yaxis.go +++ b/yaxis.go @@ -50,6 +50,8 @@ type YAxisOption struct { DivideCount int Unit int isCategoryAxis bool + // The flag for show axis split line, set this to true will show axis split line + SplitLineShow *bool } // NewYAxisOptions returns a y axis option @@ -100,6 +102,9 @@ func (opt *YAxisOption) ToAxisOption(p *Painter) AxisOption { axisOpt.StrokeWidth = 1 axisOpt.SplitLineShow = false } + if opt.SplitLineShow != nil { + axisOpt.SplitLineShow = *opt.SplitLineShow + } return axisOpt } From 06fe1006d5b2f1a79f7f68f5a986de4bd1c7b9d2 Mon Sep 17 00:00:00 2001 From: vicanso Date: Sun, 11 Feb 2024 12:39:39 +0800 Subject: [PATCH 106/116] chore: update test go version --- .github/workflows/test.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5544970..ce56fe7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,10 +20,6 @@ jobs: - '1.19' - '1.18' - '1.17' - - '1.16' - - '1.15' - - '1.14' - - '1.13' steps: - name: Go ${{ matrix.go }} test From 19a4d783fdfb2f6dedfbad90da9117d389443ffe Mon Sep 17 00:00:00 2001 From: Alexander Heidrich Date: Fri, 8 Mar 2024 20:24:13 +0100 Subject: [PATCH 107/116] fix: Label position of the pie chart --- pie_chart.go | 223 ++++++++++++++++++++++++----------- pie_chart_test.go | 294 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 447 insertions(+), 70 deletions(-) diff --git a/pie_chart.go b/pie_chart.go index b4714ac..bbf7814 100644 --- a/pie_chart.go +++ b/pie_chart.go @@ -63,6 +63,96 @@ func NewPieChart(p *Painter, opt PieChartOption) *pieChart { } } +type sector struct { + value float64 + percent float64 + cx int + cy int + rx float64 + ry float64 + start float64 + delta float64 + offset int + quadrant int + lineStartX int + lineStartY int + lineBranchX int + lineBranchY int + lineEndX int + lineEndY int + showLabel bool + label string + series Series + color Color +} + +func NewSector(cx int, cy int, radius float64, labelRadius float64, value float64, currentValue float64, totalValue float64, labelLineLength int, label string, series Series, color Color) sector { + s := sector{} + s.value = value + s.percent = value / totalValue + s.cx = cx + s.cy = cy + s.rx = radius + s.ry = radius + p := currentValue / totalValue + if p < 0.25 { + s.quadrant = 1 + } else if p < 0.5 { + s.quadrant = 4 + } else if p < 0.75 { + s.quadrant = 3 + } else { + s.quadrant = 2 + } + s.start = chart.PercentToRadians(currentValue/totalValue) - math.Pi/2 // Bogenmaß + s.delta = chart.PercentToRadians(value / totalValue) + angle := s.start + s.delta/2 + s.lineStartX = cx + int(radius*math.Cos(angle)) + s.lineStartY = cy + int(radius*math.Sin(angle)) + s.lineBranchX = cx + int(labelRadius*math.Cos(angle)) + s.lineBranchY = cy + int(labelRadius*math.Sin(angle)) + s.offset = labelLineLength + if s.lineBranchX <= cx { + s.offset *= -1 + } + s.lineEndX = s.lineBranchX + s.offset + s.lineEndY = s.lineBranchY + s.series = series + s.color = color + s.showLabel = series.Label.Show + s.label = NewPieLabelFormatter([]string{label}, series.Label.Formatter)(0, s.value, s.percent) + return s +} + +func (s *sector) calculateY(prevY int) int { + for i := 0; i <= s.cy; i++ { + if s.quadrant <= 2 { + if (prevY - s.lineBranchY) > labelFontSize+5 { + break + } + s.lineBranchY -= 1 + } else { + if (s.lineBranchY - prevY) > labelFontSize+5 { + break + } + s.lineBranchY += 1 + } + } + s.lineEndY = s.lineBranchY + return s.lineBranchY +} + +func (s *sector) calculateTextXY(textBox Box) (x int, y int) { + textMargin := 3 + x = s.lineEndX + textMargin + y = s.lineEndY + textBox.Height()>>1 - 1 + if s.offset < 0 { + textWidth := textBox.Width() + x = s.lineEndX - textWidth - textMargin + } + return +} + func (p *pieChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) { opt := p.opt values := make([]float64, len(seriesList)) @@ -101,98 +191,91 @@ func (p *pieChart) render(result *defaultRenderResult, seriesList SeriesList) (B theme := opt.Theme currentValue := float64(0) - prevPoints := make([]Point, 0) - isOverride := func(x, y int) bool { - for _, p := range prevPoints { - if math.Abs(float64(p.Y-y)) > labelFontSize { - continue - } - // label可能较多内容,不好计算横向占用空间 - // 因此x的位置需要中间位置两侧,否则认为override - if (p.X <= cx && x <= cx) || - (p.X > cx && x > cx) { - return true + var quadrant1, quadrant2, quadrant3, quadrant4 []sector + for index, v := range values { + series := seriesList[index] + color := theme.GetSeriesColor(index) + if index == len(values)-1 { + if color == theme.GetSeriesColor(0) { + color = theme.GetSeriesColor(1) } } - return false + s := NewSector(cx, cy, radius, labelRadius, v, currentValue, total, labelLineWidth, seriesNames[index], series, color) + switch quadrant := s.quadrant; quadrant { + case 1: + quadrant1 = append([]sector{s}, quadrant1...) + case 2: + quadrant2 = append(quadrant2, s) + case 3: + quadrant3 = append([]sector{s}, quadrant3...) + case 4: + quadrant4 = append(quadrant4, s) + } + currentValue += v } + sectors := append(quadrant1, quadrant4...) + sectors = append(sectors, quadrant3...) + sectors = append(sectors, quadrant2...) - for index, v := range values { + currentQuadrant := 0 + prevY := 0 + maxY := 0 + minY := 0 + for _, s := range sectors { seriesPainter.OverrideDrawingStyle(Style{ StrokeWidth: 1, - StrokeColor: theme.GetSeriesColor(index), - FillColor: theme.GetSeriesColor(index), + StrokeColor: s.color, + FillColor: s.color, }) - seriesPainter.MoveTo(cx, cy) - start := chart.PercentToRadians(currentValue/total) - math.Pi/2 - currentValue += v - percent := (v / total) - delta := chart.PercentToRadians(percent) - seriesPainter.ArcTo(cx, cy, radius, radius, start, delta). - LineTo(cx, cy). - Close(). - FillStroke() - - series := seriesList[index] - // 是否显示label - showLabel := series.Label.Show - if !showLabel { + seriesPainter.MoveTo(s.cx, s.cy) + seriesPainter.ArcTo(s.cx, s.cy, s.rx, s.ry, s.start, s.delta).LineTo(s.cx, s.cy).Close().FillStroke() + if !s.showLabel { continue } - - // label的角度为饼块中间 - angle := start + delta/2 - startx := cx + int(radius*math.Cos(angle)) - starty := cy + int(radius*math.Sin(angle)) - - endx := cx + int(labelRadius*math.Cos(angle)) - endy := cy + int(labelRadius*math.Sin(angle)) - // 计算是否有重叠,如果有则调整y坐标位置 - // 最多只尝试5次 - for i := 0; i < 5; i++ { - if !isOverride(endx, endy) { - break + if currentQuadrant != s.quadrant { + currentQuadrant = s.quadrant + if s.quadrant == 1 { + minY = cy * 2 + maxY = 0 + prevY = cy * 2 + } + if s.quadrant == 2 { + prevY = minY + } + if s.quadrant == 3 { + minY = cy * 2 + maxY = 0 + prevY = 0 + } + if s.quadrant == 4 { + prevY = maxY } - endy -= (labelFontSize << 1) } - prevPoints = append(prevPoints, Point{ - X: endx, - Y: endy, - }) - - seriesPainter.MoveTo(startx, starty) - seriesPainter.LineTo(endx, endy) - offset := labelLineWidth - if endx < cx { - offset *= -1 + prevY = s.calculateY(prevY) + if prevY > maxY { + maxY = prevY } - seriesPainter.MoveTo(endx, endy) - endx += offset - seriesPainter.LineTo(endx, endy) + if prevY < minY { + minY = prevY + } + seriesPainter.MoveTo(s.lineStartX, s.lineStartY) + seriesPainter.LineTo(s.lineBranchX, s.lineBranchY) + seriesPainter.MoveTo(s.lineBranchX, s.lineBranchY) + seriesPainter.LineTo(s.lineEndX, s.lineEndY) seriesPainter.Stroke() - textStyle := Style{ FontColor: theme.GetTextColor(), FontSize: labelFontSize, Font: opt.Font, } - if !series.Label.Color.IsZero() { - textStyle.FontColor = series.Label.Color + if !s.series.Label.Color.IsZero() { + textStyle.FontColor = s.series.Label.Color } seriesPainter.OverrideTextStyle(textStyle) - text := NewPieLabelFormatter(seriesNames, series.Label.Formatter)(index, v, percent) - textBox := seriesPainter.MeasureText(text) - textMargin := 3 - x := endx + textMargin - y := endy + textBox.Height()>>1 - 1 - if offset < 0 { - textWidth := textBox.Width() - x = endx - textWidth - textMargin - } - seriesPainter.Text(text, x, y) + x, y := s.calculateTextXY(seriesPainter.MeasureText(s.label)) + seriesPainter.Text(s.label, x, y) } - return p.p.box, nil } diff --git a/pie_chart_test.go b/pie_chart_test.go index c373a7e..0b8f798 100644 --- a/pie_chart_test.go +++ b/pie_chart_test.go @@ -23,6 +23,7 @@ package charts import ( + "strconv" "testing" "github.com/stretchr/testify/assert" @@ -98,3 +99,296 @@ func TestPieChart(t *testing.T) { assert.Equal(tt.result, string(data)) } } + +func TestPieChartWithLabelsValuesSortedDescending(t *testing.T) { + assert := assert.New(t) + + tests := []struct { + render func(*Painter) ([]byte, error) + result string + }{ + { + render: func(p *Painter) ([]byte, error) { + values := []float64{ + 84358845, + 68070697, + 58850717, + 48059777, + 36753736, + 19051562, + 17947406, + 11754004, + 10827529, + 10521556, + 10467366, + 10394055, + 9597085, + 9104772, + 6447710, + 5932654, + 5563970, + 5428792, + 5194336, + 3850894, + 2857279, + 2116792, + 1883008, + 1373101, + 920701, + 660809, + 542051, + } + _, err := NewPieChart(p, PieChartOption{ + SeriesList: NewPieSeriesList(values, PieSeriesOption{ + Label: SeriesLabel{ + Show: true, + Formatter: "{b} ({c} ≅ {d})", + }, + Radius: "200", + }), + Title: TitleOption{ + Text: "European Union member states by population", + Left: PositionRight, + }, + Padding: Box{ + Top: 20, + Right: 20, + Bottom: 20, + Left: 20, + }, + Legend: LegendOption{ + Data: []string{ + "Germany", + "France", + "Italy", + "Spain", + "Poland", + "Romania", + "Netherlands", + "Belgium", + "Czech Republic", + "Sweden", + "Portugal", + "Greece", + "Hungary", + "Austria", + "Bulgaria", + "Denmark", + "Finland", + "Slovakia", + "Ireland", + "Croatia", + "Lithuania", + "Slovenia", + "Latvia", + "Estonia", + "Cyprus", + "Luxembourg", + "Malta", + }, + Show: FalseFlag(), + }, + }).Render() + if err != nil { + return nil, err + } + return p.Bytes() + }, + result: "\\nEuropean Union member states by populationFrance (68070697 ≅ 15.17%)Germany (84358845 ≅ 18.8%)Italy (58850717 ≅ 13.12%)Spain (48059777 ≅ 10.71%)Belgium (11754004 ≅ 2.62%)Netherlands (17947406 ≅ 4%)Romania (19051562 ≅ 4.24%)Poland (36753736 ≅ 8.19%)Czech Republic (10827529 ≅ 2.41%)Sweden (10521556 ≅ 2.34%)Portugal (10467366 ≅ 2.33%)Greece (10394055 ≅ 2.31%)Hungary (9597085 ≅ 2.13%)Austria (9104772 ≅ 2.02%)Bulgaria (6447710 ≅ 1.43%)Denmark (5932654 ≅ 1.32%)Finland (5563970 ≅ 1.24%)Slovakia (5428792 ≅ 1.21%)Ireland (5194336 ≅ 1.15%)Croatia (3850894 ≅ 0.85%)Lithuania (2857279 ≅ 0.63%)Slovenia (2116792 ≅ 0.47%)Latvia (1883008 ≅ 0.41%)Estonia (1373101 ≅ 0.3%)Cyprus (920701 ≅ 0.2%)Luxembourg (660809 ≅ 0.14%)Malta (542051 ≅ 0.12%)", + }, + } + for _, tt := range tests { + p, err := NewPainter(PainterOptions{ + Type: ChartOutputSVG, + Width: 1000, + Height: 800, + }, PainterThemeOption(defaultTheme)) + assert.Nil(err) + data, err := tt.render(p.Child(PainterPaddingOption(Box{ + Left: 20, + Top: 20, + Right: 20, + Bottom: 20, + }))) + assert.Nil(err) + assert.Equal(tt.result, string(data)) + } +} + +func TestPieChartWithLabelsValuesUnsorted(t *testing.T) { + assert := assert.New(t) + + tests := []struct { + render func(*Painter) ([]byte, error) + result string + }{ + { + render: func(p *Painter) ([]byte, error) { + values := []float64{ + 9104772, + 11754004, + 6447710, + 3850894, + 920701, + 10827529, + 5932654, + 1373101, + 5563970, + 68070697, + 84358845, + 10394055, + 9597085, + 5194336, + 58850717, + 1883008, + 2857279, + 660809, + 542051, + 17947406, + 36753736, + 10467366, + 19051562, + 5428792, + 2116792, + 48059777, + 10521556, + } + _, err := NewPieChart(p, PieChartOption{ + SeriesList: NewPieSeriesList(values, PieSeriesOption{ + Label: SeriesLabel{ + Show: true, + Formatter: "{b} ({c} ≅ {d})", + }, + Radius: "200", + }), + Title: TitleOption{ + Text: "European Union member states by population", + Left: PositionRight, + }, + Padding: Box{ + Top: 20, + Right: 20, + Bottom: 20, + Left: 20, + }, + Legend: LegendOption{ + Data: []string{ + "Austria", + "Belgium", + "Bulgaria", + "Croatia", + "Cyprus", + "Czech Republic", + "Denmark", + "Estonia", + "Finland", + "France", + "Germany", + "Greece", + "Hungary", + "Ireland", + "Italy", + "Latvia", + "Lithuania", + "Luxembourg", + "Malta", + "Netherlands", + "Poland", + "Portugal", + "Romania", + "Slovakia", + "Slovenia", + "Spain", + "Sweden", + }, + Show: FalseFlag(), + }, + }).Render() + if err != nil { + return nil, err + } + return p.Bytes() + }, + result: "\\nEuropean Union member states by populationFrance (68070697 ≅ 15.17%)Finland (5563970 ≅ 1.24%)Estonia (1373101 ≅ 0.3%)Denmark (5932654 ≅ 1.32%)Czech Republic (10827529 ≅ 2.41%)Cyprus (920701 ≅ 0.2%)Croatia (3850894 ≅ 0.85%)Bulgaria (6447710 ≅ 1.43%)Belgium (11754004 ≅ 2.62%)Austria (9104772 ≅ 2.02%)Germany (84358845 ≅ 18.8%)Greece (10394055 ≅ 2.31%)Hungary (9597085 ≅ 2.13%)Poland (36753736 ≅ 8.19%)Netherlands (17947406 ≅ 4%)Malta (542051 ≅ 0.12%)Luxembourg (660809 ≅ 0.14%)Lithuania (2857279 ≅ 0.63%)Latvia (1883008 ≅ 0.41%)Italy (58850717 ≅ 13.12%)Ireland (5194336 ≅ 1.15%)Portugal (10467366 ≅ 2.33%)Romania (19051562 ≅ 4.24%)Slovakia (5428792 ≅ 1.21%)Slovenia (2116792 ≅ 0.47%)Spain (48059777 ≅ 10.71%)Sweden (10521556 ≅ 2.34%)", + }, + } + for _, tt := range tests { + p, err := NewPainter(PainterOptions{ + Type: ChartOutputSVG, + Width: 1000, + Height: 800, + }, PainterThemeOption(defaultTheme)) + assert.Nil(err) + data, err := tt.render(p.Child(PainterPaddingOption(Box{ + Left: 20, + Top: 20, + Right: 20, + Bottom: 20, + }))) + assert.Nil(err) + assert.Equal(tt.result, string(data)) + } +} + +func TestPieChartWith100Labels(t *testing.T) { + assert := assert.New(t) + + tests := []struct { + render func(*Painter) ([]byte, error) + result string + }{ + { + render: func(p *Painter) ([]byte, error) { + var values []float64 + var labels []string + for i := 1; i <= 100; i++ { + values = append(values, float64(1)) + labels = append(labels, "Label "+strconv.Itoa(i)) + } + _, err := NewPieChart(p, PieChartOption{ + SeriesList: NewPieSeriesList(values, PieSeriesOption{ + Label: SeriesLabel{ + Show: true, + }, + Radius: "200", + }), + Title: TitleOption{ + Text: "Test with 100 labels", + Left: PositionRight, + }, + Padding: Box{ + Top: 20, + Right: 20, + Bottom: 20, + Left: 20, + }, + Legend: LegendOption{ + Data: labels, + Show: FalseFlag(), + }, + }).Render() + if err != nil { + return nil, err + } + return p.Bytes() + }, + result: "\\nTest with 100 labelsLabel 25: 1%Label 24: 1%Label 23: 1%Label 22: 1%Label 21: 1%Label 20: 1%Label 19: 1%Label 18: 1%Label 17: 1%Label 16: 1%Label 15: 1%Label 14: 1%Label 13: 1%Label 12: 1%Label 11: 1%Label 10: 1%Label 9: 1%Label 8: 1%Label 7: 1%Label 6: 1%Label 5: 1%Label 4: 1%Label 3: 1%Label 2: 1%Label 1: 1%Label 26: 1%Label 27: 1%Label 28: 1%Label 29: 1%Label 30: 1%Label 31: 1%Label 32: 1%Label 33: 1%Label 34: 1%Label 35: 1%Label 36: 1%Label 37: 1%Label 38: 1%Label 39: 1%Label 40: 1%Label 41: 1%Label 42: 1%Label 43: 1%Label 44: 1%Label 45: 1%Label 46: 1%Label 47: 1%Label 48: 1%Label 49: 1%Label 50: 1%Label 75: 1%Label 74: 1%Label 73: 1%Label 72: 1%Label 71: 1%Label 70: 1%Label 69: 1%Label 68: 1%Label 67: 1%Label 66: 1%Label 65: 1%Label 64: 1%Label 63: 1%Label 62: 1%Label 61: 1%Label 60: 1%Label 59: 1%Label 58: 1%Label 57: 1%Label 56: 1%Label 55: 1%Label 54: 1%Label 53: 1%Label 52: 1%Label 51: 1%Label 76: 1%Label 77: 1%Label 78: 1%Label 79: 1%Label 80: 1%Label 81: 1%Label 82: 1%Label 83: 1%Label 84: 1%Label 85: 1%Label 86: 1%Label 87: 1%Label 88: 1%Label 89: 1%Label 90: 1%Label 91: 1%Label 92: 1%Label 93: 1%Label 94: 1%Label 95: 1%Label 96: 1%Label 97: 1%Label 98: 1%Label 99: 1%Label 100: 1%", + }, + } + for _, tt := range tests { + p, err := NewPainter(PainterOptions{ + Type: ChartOutputSVG, + Width: 1000, + Height: 900, + }, PainterThemeOption(defaultTheme)) + assert.Nil(err) + data, err := tt.render(p.Child(PainterPaddingOption(Box{ + Left: 20, + Top: 20, + Right: 20, + Bottom: 20, + }))) + assert.Nil(err) + assert.Equal(tt.result, string(data)) + } +} From 8c6c4e007c85b7c5fabf3340fff4952fc0810592 Mon Sep 17 00:00:00 2001 From: Alexander Heidrich Date: Fri, 22 Mar 2024 08:27:06 +0100 Subject: [PATCH 108/116] fix: Label position of the pie chart --- pie_chart.go | 4 +- pie_chart_test.go | 141 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 142 insertions(+), 3 deletions(-) diff --git a/pie_chart.go b/pie_chart.go index bbf7814..6cc48c4 100644 --- a/pie_chart.go +++ b/pie_chart.go @@ -94,7 +94,7 @@ func NewSector(cx int, cy int, radius float64, labelRadius float64, value float6 s.cy = cy s.rx = radius s.ry = radius - p := currentValue / totalValue + p := (currentValue + value/2) / totalValue if p < 0.25 { s.quadrant = 1 } else if p < 0.5 { @@ -104,7 +104,7 @@ func NewSector(cx int, cy int, radius float64, labelRadius float64, value float6 } else { s.quadrant = 2 } - s.start = chart.PercentToRadians(currentValue/totalValue) - math.Pi/2 // Bogenmaß + s.start = chart.PercentToRadians(currentValue/totalValue) - math.Pi/2 s.delta = chart.PercentToRadians(value / totalValue) angle := s.start + s.delta/2 s.lineStartX = cx + int(radius*math.Cos(angle)) diff --git a/pie_chart_test.go b/pie_chart_test.go index 0b8f798..3795d32 100644 --- a/pie_chart_test.go +++ b/pie_chart_test.go @@ -194,7 +194,7 @@ func TestPieChartWithLabelsValuesSortedDescending(t *testing.T) { } return p.Bytes() }, - result: "\\nEuropean Union member states by populationFrance (68070697 ≅ 15.17%)Germany (84358845 ≅ 18.8%)Italy (58850717 ≅ 13.12%)Spain (48059777 ≅ 10.71%)Belgium (11754004 ≅ 2.62%)Netherlands (17947406 ≅ 4%)Romania (19051562 ≅ 4.24%)Poland (36753736 ≅ 8.19%)Czech Republic (10827529 ≅ 2.41%)Sweden (10521556 ≅ 2.34%)Portugal (10467366 ≅ 2.33%)Greece (10394055 ≅ 2.31%)Hungary (9597085 ≅ 2.13%)Austria (9104772 ≅ 2.02%)Bulgaria (6447710 ≅ 1.43%)Denmark (5932654 ≅ 1.32%)Finland (5563970 ≅ 1.24%)Slovakia (5428792 ≅ 1.21%)Ireland (5194336 ≅ 1.15%)Croatia (3850894 ≅ 0.85%)Lithuania (2857279 ≅ 0.63%)Slovenia (2116792 ≅ 0.47%)Latvia (1883008 ≅ 0.41%)Estonia (1373101 ≅ 0.3%)Cyprus (920701 ≅ 0.2%)Luxembourg (660809 ≅ 0.14%)Malta (542051 ≅ 0.12%)", + result: "\\nEuropean Union member states by populationGermany (84358845 ≅ 18.8%)France (68070697 ≅ 15.17%)Italy (58850717 ≅ 13.12%)Netherlands (17947406 ≅ 4%)Romania (19051562 ≅ 4.24%)Poland (36753736 ≅ 8.19%)Spain (48059777 ≅ 10.71%)Belgium (11754004 ≅ 2.62%)Czech Republic (10827529 ≅ 2.41%)Sweden (10521556 ≅ 2.34%)Portugal (10467366 ≅ 2.33%)Greece (10394055 ≅ 2.31%)Hungary (9597085 ≅ 2.13%)Austria (9104772 ≅ 2.02%)Bulgaria (6447710 ≅ 1.43%)Denmark (5932654 ≅ 1.32%)Finland (5563970 ≅ 1.24%)Slovakia (5428792 ≅ 1.21%)Ireland (5194336 ≅ 1.15%)Croatia (3850894 ≅ 0.85%)Lithuania (2857279 ≅ 0.63%)Slovenia (2116792 ≅ 0.47%)Latvia (1883008 ≅ 0.41%)Estonia (1373101 ≅ 0.3%)Cyprus (920701 ≅ 0.2%)Luxembourg (660809 ≅ 0.14%)Malta (542051 ≅ 0.12%)", }, } for _, tt := range tests { @@ -392,3 +392,142 @@ func TestPieChartWith100Labels(t *testing.T) { assert.Equal(tt.result, string(data)) } } + +func TestPieChartFixLabelPos72586(t *testing.T) { + assert := assert.New(t) + + tests := []struct { + render func(*Painter) ([]byte, error) + result string + }{ + { + render: func(p *Painter) ([]byte, error) { + values := []float64{ + 397594, + 185596, + 149086, + 144258, + 120194, + 117514, + 99412, + 91135, + 87282, + 76790, + 72586, + 58818, + 58270, + 56306, + 55486, + 54792, + 53746, + 51460, + 41242, + 39476, + 37414, + 36644, + 33784, + 32788, + 32566, + 29608, + 29558, + 29384, + 28166, + 26998, + 26948, + 26054, + 25804, + 25730, + 24438, + 23782, + 22896, + 21404, + 428978, + } + _, err := NewPieChart(p, PieChartOption{ + SeriesList: NewPieSeriesList(values, PieSeriesOption{ + Label: SeriesLabel{ + Show: true, + Formatter: "{b} ({c} ≅ {d})", + }, + Radius: "150", + }), + Title: TitleOption{ + Text: "Fix label K (72586)", + Left: PositionRight, + }, + Padding: Box{ + Top: 20, + Right: 20, + Bottom: 20, + Left: 20, + }, + Legend: LegendOption{ + Data: []string{ + "A", + "B", + "C", + "D", + "E", + "F", + "G", + "H", + "I", + "J", + "K", + "L", + "M", + "N", + "O", + "P", + "Q", + "R", + "S", + "T", + "U", + "V", + "W", + "X", + "Y", + "Z", + "AA", + "AB", + "AC", + "AD", + "AE", + "AF", + "AG", + "AH", + "AI", + "AJ", + "AK", + "AL", + "AM", + }, + Show: FalseFlag(), + }, + }).Render() + if err != nil { + return nil, err + } + return p.Bytes() + }, + result: "\\nFix label K (72586)C (149086 ≅ 5.04%)B (185596 ≅ 6.28%)A (397594 ≅ 13.45%)D (144258 ≅ 4.88%)E (120194 ≅ 4.06%)F (117514 ≅ 3.97%)G (99412 ≅ 3.36%)H (91135 ≅ 3.08%)I (87282 ≅ 2.95%)J (76790 ≅ 2.59%)Z (29608 ≅ 1%)Y (32566 ≅ 1.1%)X (32788 ≅ 1.1%)W (33784 ≅ 1.14%)V (36644 ≅ 1.24%)U (37414 ≅ 1.26%)T (39476 ≅ 1.33%)S (41242 ≅ 1.39%)R (51460 ≅ 1.74%)Q (53746 ≅ 1.81%)P (54792 ≅ 1.85%)O (55486 ≅ 1.87%)N (56306 ≅ 1.9%)M (58270 ≅ 1.97%)L (58818 ≅ 1.99%)K (72586 ≅ 2.45%)AA (29558 ≅ 1%)AB (29384 ≅ 0.99%)AC (28166 ≅ 0.95%)AD (26998 ≅ 0.91%)AE (26948 ≅ 0.91%)AF (26054 ≅ 0.88%)AG (25804 ≅ 0.87%)AH (25730 ≅ 0.87%)AI (24438 ≅ 0.82%)AJ (23782 ≅ 0.8%)AK (22896 ≅ 0.77%)AL (21404 ≅ 0.72%)AM (428978 ≅ 14.52%)", + }, + } + for _, tt := range tests { + p, err := NewPainter(PainterOptions{ + Type: ChartOutputSVG, + Width: 1150, + Height: 550, + }, PainterThemeOption(defaultTheme)) + assert.Nil(err) + data, err := tt.render(p.Child(PainterPaddingOption(Box{ + Left: 20, + Top: 20, + Right: 20, + Bottom: 20, + }))) + assert.Nil(err) + assert.Equal(tt.result, string(data)) + } +} From 9b7634c2c230a0a1907c6e99be6bdea11f811a9e Mon Sep 17 00:00:00 2001 From: vicanso Date: Thu, 16 May 2024 20:02:24 +0800 Subject: [PATCH 109/116] feat: support rounded rect for bar chart --- bar_chart.go | 27 ++++++++++++------ bar_chart_test.go | 70 +++++++++++++++++++++++++++++++++++++++++++++++ painter.go | 42 ++++++++++++++++++++++++++++ painter_test.go | 23 ++++++++++++++++ series.go | 2 ++ 5 files changed, 156 insertions(+), 8 deletions(-) diff --git a/bar_chart.go b/bar_chart.go index efeb465..508c63e 100644 --- a/bar_chart.go +++ b/bar_chart.go @@ -142,14 +142,25 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B } top := barMaxHeight - h - seriesPainter.OverrideDrawingStyle(Style{ - FillColor: fillColor, - }).Rect(chart.Box{ - Top: top, - Left: x, - Right: x + barWidth, - Bottom: barMaxHeight - 1, - }) + if series.RoundRadius <= 0 { + seriesPainter.OverrideDrawingStyle(Style{ + FillColor: fillColor, + }).Rect(chart.Box{ + Top: top, + Left: x, + Right: x + barWidth, + Bottom: barMaxHeight - 1, + }) + } else { + seriesPainter.OverrideDrawingStyle(Style{ + FillColor: fillColor, + }).RoundedRect(chart.Box{ + Top: top, + Left: x, + Right: x + barWidth, + Bottom: barMaxHeight - 1, + }, series.RoundRadius) + } // 用于生成marker point points[j] = Point{ // 居中的位置 diff --git a/bar_chart_test.go b/bar_chart_test.go index e1522d6..654c320 100644 --- a/bar_chart_test.go +++ b/bar_chart_test.go @@ -104,6 +104,76 @@ func TestBarChart(t *testing.T) { }, result: "\\n24020016012080400FebMayAugNov24.9723.225.676.7135.6162.232.6206.43.32.65.9926.428.770.7175.6182.248.718.862.3", }, + { + render: func(p *Painter) ([]byte, error) { + seriesList := NewSeriesListDataFromValues([][]float64{ + { + 2.0, + 4.9, + 7.0, + 23.2, + 25.6, + 76.7, + 135.6, + 162.2, + 32.6, + 20.0, + 6.4, + 3.3, + }, + { + 2.6, + 5.9, + 9.0, + 26.4, + 28.7, + 70.7, + 175.6, + 182.2, + 48.7, + 18.8, + 6.0, + 2.3, + }, + }) + for index := range seriesList { + seriesList[index].Label.Show = true + seriesList[index].RoundRadius = 5 + } + _, err := NewBarChart(p, BarChartOption{ + Padding: Box{ + Left: 10, + Top: 10, + Right: 10, + Bottom: 10, + }, + SeriesList: seriesList, + XAxis: NewXAxisOption([]string{ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + }), + YAxisOptions: NewYAxisOptions([]string{ + "Rainfall", + "Evaporation", + }), + }).Render() + if err != nil { + return nil, err + } + return p.Bytes() + }, + result: "\\n24020016012080400FebMayAugNov24.9723.225.676.7135.6162.232.6206.43.32.65.9926.428.770.7175.6182.248.718.862.3", + }, } for _, tt := range tests { diff --git a/painter.go b/painter.go index 18496fd..bc13418 100644 --- a/painter.go +++ b/painter.go @@ -803,6 +803,48 @@ func (p *Painter) Rect(box Box) *Painter { return p } +func (p *Painter) RoundedRect(box Box, radius int) *Painter { + r := (box.Right - box.Left) / 2 + if radius > r { + radius = r + } + rx := float64(radius) + ry := float64(radius) + p.MoveTo(box.Left+radius, box.Top) + p.LineTo(box.Right-radius, box.Top) + + cx := box.Right - radius + cy := box.Top + radius + // right top + p.ArcTo(cx, cy, rx, ry, -math.Pi/2, math.Pi/2) + + p.LineTo(box.Right, box.Bottom-radius) + + // right bottom + cx = box.Right - radius + cy = box.Bottom - radius + p.ArcTo(cx, cy, rx, ry, 0.0, math.Pi/2) + + p.LineTo(box.Left+radius, box.Bottom) + + // left bottom + cx = box.Left + radius + cy = box.Bottom - radius + p.ArcTo(cx, cy, rx, ry, math.Pi/2, math.Pi/2) + + p.LineTo(box.Left, box.Top+radius) + + // left top + cx = box.Left + radius + cy = box.Top + radius + p.ArcTo(cx, cy, rx, ry, math.Pi, math.Pi/2) + + p.Close() + p.FillStroke() + p.Fill() + return p +} + func (p *Painter) LegendLineDot(box Box) *Painter { width := box.Width() height := box.Height() diff --git a/painter_test.go b/painter_test.go index 2392d5b..b159328 100644 --- a/painter_test.go +++ b/painter_test.go @@ -343,6 +343,29 @@ func TestPainter(t *testing.T) { } } +func TestRoundedRect(t *testing.T) { + assert := assert.New(t) + p, err := NewPainter(PainterOptions{ + Width: 400, + Height: 300, + Type: ChartOutputSVG, + }) + assert.Nil(err) + p.OverrideDrawingStyle(Style{ + FillColor: drawing.ColorWhite, + StrokeWidth: 1, + StrokeColor: drawing.ColorWhite, + }).RoundedRect(Box{ + Left: 10, + Right: 30, + Bottom: 150, + Top: 10, + }, 5) + buf, err := p.Bytes() + assert.Nil(err) + assert.Equal("\\n", string(buf)) +} + func TestPainterTextFit(t *testing.T) { assert := assert.New(t) p, err := NewPainter(PainterOptions{ diff --git a/series.go b/series.go index f28bfa9..0ad135f 100644 --- a/series.go +++ b/series.go @@ -126,6 +126,8 @@ type Series struct { Name string // Radius for Pie chart, e.g.: 40%, default is "40%" Radius string + // Round for bar chart + RoundRadius int // Mark point for series MarkPoint SeriesMarkPoint // Make line for series From 96148357237a4698910b58e780f6a2fe095a4cfe Mon Sep 17 00:00:00 2001 From: vicanso Date: Tue, 21 May 2024 20:26:40 +0800 Subject: [PATCH 110/116] feat: support rounded rect for horizontal bar chart --- examples/horizontal_bar_chart/main.go | 3 +++ horizontal_bar_chart.go | 28 +++++++++++++++++++-------- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/examples/horizontal_bar_chart/main.go b/examples/horizontal_bar_chart/main.go index f2cabe8..a7daa8c 100644 --- a/examples/horizontal_bar_chart/main.go +++ b/examples/horizontal_bar_chart/main.go @@ -65,6 +65,9 @@ func main() { "China", "World", }), + func(opt *charts.ChartOption) { + opt.SeriesList[0].RoundRadius = 5 + }, ) if err != nil { panic(err) diff --git a/horizontal_bar_chart.go b/horizontal_bar_chart.go index 2ab4c03..ca91242 100644 --- a/horizontal_bar_chart.go +++ b/horizontal_bar_chart.go @@ -136,14 +136,26 @@ func (h *horizontalBarChart) render(result *defaultRenderResult, seriesList Seri fillColor = item.Style.FillColor } right := w - seriesPainter.OverrideDrawingStyle(Style{ - FillColor: fillColor, - }).Rect(chart.Box{ - Top: y, - Left: 0, - Right: right, - Bottom: y + barHeight, - }) + if series.RoundRadius <= 0 { + seriesPainter.OverrideDrawingStyle(Style{ + FillColor: fillColor, + }).Rect(chart.Box{ + Top: y, + Left: 0, + Right: right, + Bottom: y + barHeight, + }) + } else { + seriesPainter.OverrideDrawingStyle(Style{ + FillColor: fillColor, + }).RoundedRect(chart.Box{ + Top: y, + Left: 0, + Right: right, + Bottom: y + barHeight, + }, series.RoundRadius) + } + // 如果label不需要展示,则返回 if labelPainter == nil { continue From 32e6dd52d093ff3411bab31f2bed1357ddc29f48 Mon Sep 17 00:00:00 2001 From: vicanso Date: Wed, 5 Jun 2024 21:13:03 +0800 Subject: [PATCH 111/116] refactor: export `GetRenderer` function to get chart renderer --- painter.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/painter.go b/painter.go index bc13418..2bbbe2e 100644 --- a/painter.go +++ b/painter.go @@ -860,3 +860,7 @@ func (p *Painter) LegendLineDot(box Box) *Painter { p.FillStroke() return p } + +func (p *Painter) GetRenderer() chart.Renderer { + return p.render +} From e7dc4189d5b4bae6e8cf1b10851f6766cb3d4f39 Mon Sep 17 00:00:00 2001 From: vicanso Date: Fri, 7 Jun 2024 20:35:03 +0800 Subject: [PATCH 112/116] feat: support bar margin --- bar_chart.go | 7 +++++++ chart_option.go | 2 ++ charts.go | 10 ++++++---- horizontal_bar_chart.go | 5 +++++ 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/bar_chart.go b/bar_chart.go index 508c63e..1bebc88 100644 --- a/bar_chart.go +++ b/bar_chart.go @@ -23,6 +23,7 @@ package charts import ( + "fmt" "math" "github.com/golang/freetype/truetype" @@ -63,6 +64,8 @@ type BarChartOption struct { // The legend option Legend LegendOption BarWidth int + // Margin of bar + BarMargin int } func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) { @@ -88,6 +91,10 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B margin = 5 barMargin = 3 } + if opt.BarMargin > 0 { + barMargin = opt.BarMargin + } + fmt.Println(barMargin) seriesCount := len(seriesList) // 总的宽度-两个margin-(总数-1)的barMargin barWidth := (width - 2*margin - barMargin*(seriesCount-1)) / seriesCount diff --git a/chart_option.go b/chart_option.go index 5311d50..d80a383 100644 --- a/chart_option.go +++ b/chart_option.go @@ -67,6 +67,8 @@ type ChartOption struct { LineStrokeWidth float64 // The bar with of bar chart BarWidth int + // The margin of each bar + BarMargin int // The bar height of horizontal bar chart BarHeight int // Fill the area of line chart diff --git a/charts.go b/charts.go index 74db733..91a0048 100644 --- a/charts.go +++ b/charts.go @@ -375,10 +375,11 @@ func Render(opt ChartOption, opts ...OptionFunc) (*Painter, error) { if len(barSeriesList) != 0 { handler.Add(func() error { _, err := NewBarChart(p, BarChartOption{ - Theme: opt.theme, - Font: opt.font, - XAxis: opt.XAxis, - BarWidth: opt.BarWidth, + Theme: opt.theme, + Font: opt.font, + XAxis: opt.XAxis, + BarWidth: opt.BarWidth, + BarMargin: opt.BarMargin, }).render(renderResult, barSeriesList) return err }) @@ -391,6 +392,7 @@ func Render(opt ChartOption, opts ...OptionFunc) (*Painter, error) { Theme: opt.theme, Font: opt.font, BarHeight: opt.BarHeight, + BarMargin: opt.BarMargin, YAxisOptions: opt.YAxisOptions, }).render(renderResult, horizontalBarSeriesList) return err diff --git a/horizontal_bar_chart.go b/horizontal_bar_chart.go index ca91242..2ea97bd 100644 --- a/horizontal_bar_chart.go +++ b/horizontal_bar_chart.go @@ -50,6 +50,8 @@ type HorizontalBarChartOption struct { // The legend option Legend LegendOption BarHeight int + // Margin of bar + BarMargin int } // NewHorizontalBarChart returns a horizontal bar chart renderer @@ -81,6 +83,9 @@ func (h *horizontalBarChart) render(result *defaultRenderResult, seriesList Seri margin = 5 barMargin = 3 } + if opt.BarMargin > 0 { + barMargin = opt.BarMargin + } seriesCount := len(seriesList) // 总的高度-两个margin-(总数-1)的barMargin barHeight := (height - 2*margin - barMargin*(seriesCount-1)) / seriesCount From 5842c71b1d23147fad578d8c11e0f587c464ef71 Mon Sep 17 00:00:00 2001 From: vicanso Date: Thu, 1 Aug 2024 21:44:52 +0800 Subject: [PATCH 113/116] refactor: remove unused code --- bar_chart.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/bar_chart.go b/bar_chart.go index 1bebc88..2d702f2 100644 --- a/bar_chart.go +++ b/bar_chart.go @@ -23,7 +23,6 @@ package charts import ( - "fmt" "math" "github.com/golang/freetype/truetype" @@ -94,7 +93,6 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B if opt.BarMargin > 0 { barMargin = opt.BarMargin } - fmt.Println(barMargin) seriesCount := len(seriesList) // 总的宽度-两个margin-(总数-1)的barMargin barWidth := (width - 2*margin - barMargin*(seriesCount-1)) / seriesCount From d25a827706ac3d9c23b2676085c7d6fb845e7782 Mon Sep 17 00:00:00 2001 From: vicanso Date: Thu, 15 Aug 2024 20:37:07 +0800 Subject: [PATCH 114/116] fix: fix label position of pie, #86 --- pie_chart.go | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/pie_chart.go b/pie_chart.go index 6cc48c4..9e036c6 100644 --- a/pie_chart.go +++ b/pie_chart.go @@ -234,23 +234,35 @@ func (p *pieChart) render(result *defaultRenderResult, seriesList SeriesList) (B continue } if currentQuadrant != s.quadrant { - currentQuadrant = s.quadrant if s.quadrant == 1 { minY = cy * 2 maxY = 0 prevY = cy * 2 } if s.quadrant == 2 { - prevY = minY + if currentQuadrant != 3 { + prevY = s.lineEndY + } else { + prevY = minY + } } if s.quadrant == 3 { - minY = cy * 2 - maxY = 0 - prevY = 0 + if currentQuadrant != 4 { + prevY = s.lineEndY + } else { + minY = cy * 2 + maxY = 0 + prevY = 0 + } } if s.quadrant == 4 { - prevY = maxY + if currentQuadrant != 1 { + prevY = s.lineEndY + } else { + prevY = maxY + } } + currentQuadrant = s.quadrant } prevY = s.calculateY(prevY) if prevY > maxY { From 0eacc8e394512d513cd7fa53b8c2fe525b9b2f3c Mon Sep 17 00:00:00 2001 From: Zeni Kim Date: Tue, 13 May 2025 21:46:02 -0500 Subject: [PATCH 115/116] start migration to our packages --- README.md | 2 ++ alias.go | 4 ++-- axis.go | 2 +- axis_test.go | 2 +- bar_chart.go | 2 +- chart_option_test.go | 2 +- charts.go | 2 +- charts_test.go | 2 +- echarts.go | 2 +- echarts_test.go | 2 +- examples/painter/main.go | 2 +- examples/table/main.go | 2 +- font.go | 2 +- font_test.go | 2 +- go.mod | 12 +++++++----- go.sum | 18 ++++++++---------- grid_test.go | 2 +- horizontal_bar_chart.go | 2 +- line_chart.go | 2 +- mark_line_test.go | 2 +- mark_point_test.go | 2 +- painter.go | 2 +- painter_test.go | 4 ++-- pie_chart.go | 2 +- radar_chart.go | 4 ++-- series.go | 2 +- series_label.go | 2 +- table.go | 4 ++-- theme.go | 2 +- util.go | 4 ++-- util_test.go | 4 ++-- 31 files changed, 51 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 1e4ea8b..4cfb004 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # go-charts +Clone from https://github.com/vicanso/go-charts + [![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/vicanso/go-charts/blob/master/LICENSE) [![Build Status](https://github.com/vicanso/go-charts/workflows/Test/badge.svg)](https://github.com/vicanso/go-charts/actions) diff --git a/alias.go b/alias.go index a96f50b..edf0dec 100644 --- a/alias.go +++ b/alias.go @@ -23,8 +23,8 @@ package charts import ( - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" + "git.smarteching.com/zeni/go-chart/v2" + "git.smarteching.com/zeni/go-chart/v2/drawing" ) type Box = chart.Box diff --git a/axis.go b/axis.go index 762a6a2..55fa219 100644 --- a/axis.go +++ b/axis.go @@ -26,7 +26,7 @@ import ( "strings" "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2" + "git.smarteching.com/zeni/go-chart/v2" ) type axisPainter struct { diff --git a/axis_test.go b/axis_test.go index d0cff41..85e18ca 100644 --- a/axis_test.go +++ b/axis_test.go @@ -26,7 +26,7 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/wcharczuk/go-chart/v2/drawing" + "git.smarteching.com/zeni/go-chart/v2/drawing" ) func TestAxis(t *testing.T) { diff --git a/bar_chart.go b/bar_chart.go index 2d702f2..043e044 100644 --- a/bar_chart.go +++ b/bar_chart.go @@ -26,7 +26,7 @@ import ( "math" "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2" + "git.smarteching.com/zeni/go-chart/v2" ) type barChart struct { diff --git a/chart_option_test.go b/chart_option_test.go index ff17750..c354b26 100644 --- a/chart_option_test.go +++ b/chart_option_test.go @@ -26,7 +26,7 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/wcharczuk/go-chart/v2/drawing" + "git.smarteching.com/zeni/go-chart/v2/drawing" ) func TestChartOption(t *testing.T) { diff --git a/charts.go b/charts.go index 91a0048..31df11c 100644 --- a/charts.go +++ b/charts.go @@ -27,7 +27,7 @@ import ( "math" "sort" - "github.com/wcharczuk/go-chart/v2" + "git.smarteching.com/zeni/go-chart/v2" ) const labelFontSize = 10 diff --git a/charts_test.go b/charts_test.go index da75ee5..bd581e9 100644 --- a/charts_test.go +++ b/charts_test.go @@ -26,7 +26,7 @@ import ( "errors" "testing" - "github.com/wcharczuk/go-chart/v2" + "git.smarteching.com/zeni/go-chart/v2" ) func BenchmarkMultiChartPNGRender(b *testing.B) { diff --git a/echarts.go b/echarts.go index 5a0e5a0..aaef1f1 100644 --- a/echarts.go +++ b/echarts.go @@ -29,7 +29,7 @@ import ( "regexp" "strconv" - "github.com/wcharczuk/go-chart/v2" + "git.smarteching.com/zeni/go-chart/v2" ) func convertToArray(data []byte) []byte { diff --git a/echarts_test.go b/echarts_test.go index 2ce1715..2077278 100644 --- a/echarts_test.go +++ b/echarts_test.go @@ -27,7 +27,7 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/wcharczuk/go-chart/v2/drawing" + "git.smarteching.com/zeni/go-chart/v2/drawing" ) func TestConvertToArray(t *testing.T) { diff --git a/examples/painter/main.go b/examples/painter/main.go index b7a5832..193eb7c 100644 --- a/examples/painter/main.go +++ b/examples/painter/main.go @@ -5,7 +5,7 @@ import ( "path/filepath" charts "github.com/vicanso/go-charts/v2" - "github.com/wcharczuk/go-chart/v2/drawing" + "git.smarteching.com/zeni/go-chart/v2/drawing" ) func writeFile(buf []byte) error { diff --git a/examples/table/main.go b/examples/table/main.go index 0210ecf..036dc90 100644 --- a/examples/table/main.go +++ b/examples/table/main.go @@ -7,7 +7,7 @@ import ( "strings" "github.com/vicanso/go-charts/v2" - "github.com/wcharczuk/go-chart/v2/drawing" + "git.smarteching.com/zeni/go-chart/v2/drawing" ) func writeFile(buf []byte, filename string) error { diff --git a/font.go b/font.go index dae5141..828654e 100644 --- a/font.go +++ b/font.go @@ -27,7 +27,7 @@ import ( "sync" "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2/roboto" + "git.smarteching.com/zeni/go-chart/v2/roboto" ) var fonts = sync.Map{} diff --git a/font_test.go b/font_test.go index 9dc731c..e0c56b2 100644 --- a/font_test.go +++ b/font_test.go @@ -26,7 +26,7 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/wcharczuk/go-chart/v2/roboto" + "git.smarteching.com/zeni/go-chart/v2/roboto" ) func TestInstallFont(t *testing.T) { diff --git a/go.mod b/go.mod index d8a492c..b984e59 100644 --- a/go.mod +++ b/go.mod @@ -1,17 +1,19 @@ -module github.com/vicanso/go-charts/v2 +module git.smarteching.com/zeni/go-charts/v2 -go 1.17 +go 1.24.1 require ( + git.smarteching.com/zeni/go-chart/v2 v2.1.4 github.com/dustin/go-humanize v1.0.1 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 - github.com/stretchr/testify v1.8.2 - github.com/wcharczuk/go-chart/v2 v2.1.0 + github.com/stretchr/testify v1.10.0 + github.com/vicanso/go-charts/v2 v2.6.10 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 // indirect + github.com/wcharczuk/go-chart/v2 v2.1.0 // indirect + golang.org/x/image v0.21.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index ac1d9f7..e7a75a8 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,5 @@ -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +git.smarteching.com/zeni/go-chart/v2 v2.1.4 h1:pF06+F6eqJLIG8uMiTVPR5TygPGMjM/FHMzTxmu5V/Q= +git.smarteching.com/zeni/go-chart/v2 v2.1.4/go.mod h1:b3ueW9h3pGGXyhkormZAvilHaG4+mQti+bMNPdQBeOQ= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= @@ -7,20 +8,17 @@ github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF0 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/vicanso/go-charts/v2 v2.6.10 h1:Nb2YBekEbUBPbvohnUO1oYMy31v75brUPk6n/fq+JXw= +github.com/vicanso/go-charts/v2 v2.6.10/go.mod h1:Ii2KDI3udTG1wPtiTnntzjlUBJVJTqNscMzh3oYHzUk= github.com/wcharczuk/go-chart/v2 v2.1.0 h1:tY2slqVQ6bN+yHSnDYwZebLQFkphK4WNrVwnt7CJZ2I= github.com/wcharczuk/go-chart/v2 v2.1.0/go.mod h1:yx7MvAVNcP/kN9lKXM/NTce4au4DFN99j6i1OwDclNA= -golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 h1:QelT11PB4FXiDEXucrfNckHoFxwt8USGY1ajP1ZF5lM= golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s= +golang.org/x/image v0.21.0/go.mod h1:vUbsLavqK/W303ZroQQVKQ+Af3Yl6Uz1Ppu5J/cLz78= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/grid_test.go b/grid_test.go index 3110a2b..fa9c3a6 100644 --- a/grid_test.go +++ b/grid_test.go @@ -26,7 +26,7 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/wcharczuk/go-chart/v2/drawing" + "git.smarteching.com/zeni/go-chart/v2/drawing" ) func TestGrid(t *testing.T) { diff --git a/horizontal_bar_chart.go b/horizontal_bar_chart.go index 2ea97bd..ed091c9 100644 --- a/horizontal_bar_chart.go +++ b/horizontal_bar_chart.go @@ -24,7 +24,7 @@ package charts import ( "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2" + "git.smarteching.com/zeni/go-chart/v2" ) type horizontalBarChart struct { diff --git a/line_chart.go b/line_chart.go index 363cd36..fb1d16a 100644 --- a/line_chart.go +++ b/line_chart.go @@ -26,7 +26,7 @@ import ( "math" "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2/drawing" + "git.smarteching.com/zeni/go-chart/v2/drawing" ) type lineChart struct { diff --git a/mark_line_test.go b/mark_line_test.go index 00d19ef..0448cda 100644 --- a/mark_line_test.go +++ b/mark_line_test.go @@ -26,7 +26,7 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/wcharczuk/go-chart/v2/drawing" + "git.smarteching.com/zeni/go-chart/v2/drawing" ) func TestMarkLine(t *testing.T) { diff --git a/mark_point_test.go b/mark_point_test.go index ffa01a7..298345b 100644 --- a/mark_point_test.go +++ b/mark_point_test.go @@ -26,7 +26,7 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/wcharczuk/go-chart/v2/drawing" + "git.smarteching.com/zeni/go-chart/v2/drawing" ) func TestMarkPoint(t *testing.T) { diff --git a/painter.go b/painter.go index 2bbbe2e..bee646f 100644 --- a/painter.go +++ b/painter.go @@ -28,7 +28,7 @@ import ( "math" "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2" + "git.smarteching.com/zeni/go-chart/v2" ) type ValueFormatter func(float64) string diff --git a/painter_test.go b/painter_test.go index b159328..07c4113 100644 --- a/painter_test.go +++ b/painter_test.go @@ -28,8 +28,8 @@ import ( "github.com/golang/freetype/truetype" "github.com/stretchr/testify/assert" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" + "git.smarteching.com/zeni/go-chart/v2" + "git.smarteching.com/zeni/go-chart/v2/drawing" ) func TestPainterOption(t *testing.T) { diff --git a/pie_chart.go b/pie_chart.go index 9e036c6..5c04ed8 100644 --- a/pie_chart.go +++ b/pie_chart.go @@ -27,7 +27,7 @@ import ( "math" "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2" + "git.smarteching.com/zeni/go-chart/v2" ) type pieChart struct { diff --git a/radar_chart.go b/radar_chart.go index f3d63b9..cf18135 100644 --- a/radar_chart.go +++ b/radar_chart.go @@ -27,8 +27,8 @@ import ( "github.com/dustin/go-humanize" "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" + "git.smarteching.com/zeni/go-chart/v2" + "git.smarteching.com/zeni/go-chart/v2/drawing" ) type radarChart struct { diff --git a/series.go b/series.go index 0ad135f..da50e64 100644 --- a/series.go +++ b/series.go @@ -26,7 +26,7 @@ import ( "strings" "github.com/dustin/go-humanize" - "github.com/wcharczuk/go-chart/v2" + "git.smarteching.com/zeni/go-chart/v2" ) type SeriesData struct { diff --git a/series_label.go b/series_label.go index 10fd148..af873fc 100644 --- a/series_label.go +++ b/series_label.go @@ -24,7 +24,7 @@ package charts import ( "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2" + "git.smarteching.com/zeni/go-chart/v2" ) type labelRenderValue struct { diff --git a/table.go b/table.go index 86ef569..3e6f273 100644 --- a/table.go +++ b/table.go @@ -26,8 +26,8 @@ import ( "errors" "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" + "git.smarteching.com/zeni/go-chart/v2" + "git.smarteching.com/zeni/go-chart/v2/drawing" ) type tableChart struct { diff --git a/theme.go b/theme.go index a6d624f..85016a5 100644 --- a/theme.go +++ b/theme.go @@ -24,7 +24,7 @@ package charts import ( "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2/drawing" + "git.smarteching.com/zeni/go-chart/v2/drawing" ) const ThemeDark = "dark" diff --git a/util.go b/util.go index b333e6d..87ff31c 100644 --- a/util.go +++ b/util.go @@ -29,8 +29,8 @@ import ( "strings" "github.com/dustin/go-humanize" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" + "git.smarteching.com/zeni/go-chart/v2" + "git.smarteching.com/zeni/go-chart/v2/drawing" ) func TrueFlag() *bool { diff --git a/util_test.go b/util_test.go index 62fd08d..5770776 100644 --- a/util_test.go +++ b/util_test.go @@ -26,8 +26,8 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" + "git.smarteching.com/zeni/go-chart/v2" + "git.smarteching.com/zeni/go-chart/v2/drawing" ) func TestGetDefaultInt(t *testing.T) { From 958172a1f1206cc6b676c0c02da928ce7f21cf7f Mon Sep 17 00:00:00 2001 From: Zeni Kim Date: Tue, 13 May 2025 21:53:31 -0500 Subject: [PATCH 116/116] update URL in examples --- README.md | 16 ++++++++-------- README_zh.md | 16 ++++++++-------- examples/area_line_chart/main.go | 2 +- examples/bar_chart/main.go | 2 +- examples/charts/main.go | 2 +- examples/chinese/main.go | 2 +- examples/funnel_chart/main.go | 2 +- examples/horizontal_bar_chart/main.go | 2 +- examples/line_chart/main.go | 2 +- examples/painter/main.go | 2 +- examples/pie_chart/main.go | 2 +- examples/radar_chart/main.go | 2 +- examples/table/main.go | 2 +- examples/time_line_chart/main.go | 2 +- go.mod | 2 -- go.sum | 6 ------ 16 files changed, 28 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 4cfb004..0650395 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ More examples can be found in the [./examples/](./examples/) directory. package main import ( - charts "github.com/vicanso/go-charts/v2" + charts "git.smarteching.com/zeni/go-charts/v2" ) func main() { @@ -101,7 +101,7 @@ func main() { package main import ( - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func main() { @@ -176,7 +176,7 @@ func main() { package main import ( - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func main() { @@ -233,7 +233,7 @@ func main() { package main import ( - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func main() { @@ -288,7 +288,7 @@ func main() { package main import ( - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func main() { @@ -346,7 +346,7 @@ func main() { package main import ( - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func main() { @@ -386,7 +386,7 @@ func main() { package main import ( - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func main() { @@ -451,7 +451,7 @@ func main() { package main import ( - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func main() { diff --git a/README_zh.md b/README_zh.md index c31cf77..3f35b97 100644 --- a/README_zh.md +++ b/README_zh.md @@ -32,7 +32,7 @@ package main import ( - charts "github.com/vicanso/go-charts/v2" + charts "git.smarteching.com/zeni/go-charts/v2" ) func main() { @@ -98,7 +98,7 @@ func main() { package main import ( - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func main() { @@ -173,7 +173,7 @@ func main() { package main import ( - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func main() { @@ -230,7 +230,7 @@ func main() { package main import ( - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func main() { @@ -285,7 +285,7 @@ func main() { package main import ( - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func main() { @@ -343,7 +343,7 @@ func main() { package main import ( - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func main() { @@ -383,7 +383,7 @@ func main() { package main import ( - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func main() { @@ -447,7 +447,7 @@ func main() { package main import ( - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func main() { diff --git a/examples/area_line_chart/main.go b/examples/area_line_chart/main.go index ea8f1c2..57ca1e9 100644 --- a/examples/area_line_chart/main.go +++ b/examples/area_line_chart/main.go @@ -4,7 +4,7 @@ import ( "os" "path/filepath" - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func writeFile(buf []byte) error { diff --git a/examples/bar_chart/main.go b/examples/bar_chart/main.go index feea66e..91c9f81 100644 --- a/examples/bar_chart/main.go +++ b/examples/bar_chart/main.go @@ -4,7 +4,7 @@ import ( "os" "path/filepath" - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func writeFile(buf []byte) error { diff --git a/examples/charts/main.go b/examples/charts/main.go index 76aa42c..81bc4f2 100644 --- a/examples/charts/main.go +++ b/examples/charts/main.go @@ -6,7 +6,7 @@ import ( "net/http" "strconv" - charts "github.com/vicanso/go-charts/v2" + charts "git.smarteching.com/zeni/go-charts/v2" ) var html = ` diff --git a/examples/chinese/main.go b/examples/chinese/main.go index 2d96b58..601f54e 100644 --- a/examples/chinese/main.go +++ b/examples/chinese/main.go @@ -5,7 +5,7 @@ import ( "os" "path/filepath" - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func writeFile(buf []byte) error { diff --git a/examples/funnel_chart/main.go b/examples/funnel_chart/main.go index f29ccf9..653f834 100644 --- a/examples/funnel_chart/main.go +++ b/examples/funnel_chart/main.go @@ -4,7 +4,7 @@ import ( "os" "path/filepath" - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func writeFile(buf []byte) error { diff --git a/examples/horizontal_bar_chart/main.go b/examples/horizontal_bar_chart/main.go index a7daa8c..f5d8497 100644 --- a/examples/horizontal_bar_chart/main.go +++ b/examples/horizontal_bar_chart/main.go @@ -4,7 +4,7 @@ import ( "os" "path/filepath" - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func writeFile(buf []byte) error { diff --git a/examples/line_chart/main.go b/examples/line_chart/main.go index 4e6448f..baee8a3 100644 --- a/examples/line_chart/main.go +++ b/examples/line_chart/main.go @@ -5,7 +5,7 @@ import ( "os" "path/filepath" - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func writeFile(buf []byte) error { diff --git a/examples/painter/main.go b/examples/painter/main.go index 193eb7c..1b842b3 100644 --- a/examples/painter/main.go +++ b/examples/painter/main.go @@ -4,7 +4,7 @@ import ( "os" "path/filepath" - charts "github.com/vicanso/go-charts/v2" + charts "git.smarteching.com/zeni/go-charts/v2" "git.smarteching.com/zeni/go-chart/v2/drawing" ) diff --git a/examples/pie_chart/main.go b/examples/pie_chart/main.go index 38488d2..5d70438 100644 --- a/examples/pie_chart/main.go +++ b/examples/pie_chart/main.go @@ -4,7 +4,7 @@ import ( "os" "path/filepath" - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func writeFile(buf []byte) error { diff --git a/examples/radar_chart/main.go b/examples/radar_chart/main.go index e8095ae..e7053af 100644 --- a/examples/radar_chart/main.go +++ b/examples/radar_chart/main.go @@ -4,7 +4,7 @@ import ( "os" "path/filepath" - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func writeFile(buf []byte) error { diff --git a/examples/table/main.go b/examples/table/main.go index 036dc90..de994eb 100644 --- a/examples/table/main.go +++ b/examples/table/main.go @@ -6,7 +6,7 @@ import ( "strconv" "strings" - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" "git.smarteching.com/zeni/go-chart/v2/drawing" ) diff --git a/examples/time_line_chart/main.go b/examples/time_line_chart/main.go index 6cb3f3d..c6c93bf 100644 --- a/examples/time_line_chart/main.go +++ b/examples/time_line_chart/main.go @@ -8,7 +8,7 @@ import ( "path/filepath" "time" - "github.com/vicanso/go-charts/v2" + "git.smarteching.com/zeni/go-charts/v2" ) func writeFile(buf []byte) error { diff --git a/go.mod b/go.mod index b984e59..76a47b6 100644 --- a/go.mod +++ b/go.mod @@ -7,13 +7,11 @@ require ( github.com/dustin/go-humanize v1.0.1 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 github.com/stretchr/testify v1.10.0 - github.com/vicanso/go-charts/v2 v2.6.10 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/wcharczuk/go-chart/v2 v2.1.0 // indirect golang.org/x/image v0.21.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index e7a75a8..3e1a48a 100644 --- a/go.sum +++ b/go.sum @@ -10,14 +10,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/vicanso/go-charts/v2 v2.6.10 h1:Nb2YBekEbUBPbvohnUO1oYMy31v75brUPk6n/fq+JXw= -github.com/vicanso/go-charts/v2 v2.6.10/go.mod h1:Ii2KDI3udTG1wPtiTnntzjlUBJVJTqNscMzh3oYHzUk= -github.com/wcharczuk/go-chart/v2 v2.1.0 h1:tY2slqVQ6bN+yHSnDYwZebLQFkphK4WNrVwnt7CJZ2I= -github.com/wcharczuk/go-chart/v2 v2.1.0/go.mod h1:yx7MvAVNcP/kN9lKXM/NTce4au4DFN99j6i1OwDclNA= -golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s= golang.org/x/image v0.21.0/go.mod h1:vUbsLavqK/W303ZroQQVKQ+Af3Yl6Uz1Ppu5J/cLz78= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=